13
\$\begingroup\$

I find it pretty annoying that c++11 atomics can't be copied. The reasons for this have been discussed e.g. here and I don't want to argue about them now. However, I find myself repeatedly in situations, where I want to copy data structures around that contain atomics. Usually BEFORE they are actually used in a multithreaded context (e.g. in order to return them from a factory function, or to store them in a vector etc.).

In order to solve that problem without having to manually write a copy constructor over and over again, I decided to write a simple class, that publicly derives from std::atomic and adds those functionality:

/**
 * Drop in replacement for std::atomic that provides a copy constructor and copy assignment operator.
 *
 * Contrary to normal atomics, these atomics don't prevent the generation of 
 * default constructor and copy operators for classes they are members of.
 * 
 * Copying those atomics is thread safe, but be aware that 
 * it doesn't provide any form of synchronization.
 */
template<class T>
class CopyableAtomic : public std::atomic<T>
{
public:
    //defaultinitializes value
    CopyableAtomic() : 
        std::atomic<T>(T{})
    {}

    constexpr CopyableAtomic(T desired) : 
        std::atomic<T>(desired) 
    {}

    constexpr CopyableAtomic(const CopyableAtomic<T>& other) :
        CopyableAtomic(other.load(std::memory_order_relaxed))
    {}

    CopyableAtomic& operator=(const CopyableAtomic<T>& other) {
        this->store(other.load(std::memory_order_relaxed), std::memory_order_relaxed);
        return *this;
    }
};

In my toy examples this worked pretty well, however, I'm not sure, if I really considered all possible ramifications of this - deriving from standard library types feels awkward enough and when it comes to synchronization primitives I feel a bit like playing with fire (or juggling with razorblades to quote Herb Sutter).

So what I would like to know (aside from general improvement suggestions, or alternative approaches):

  • Can this really serve as a drop-in replacement everywhere, where you would use a normal atomic (especially also with CAS instructions).
  • Is my claim, that the copy constructor / assignment operator is threadsafe correct (both with respect to this as well as with respect to other)
  • Can you think of any performance regressions that this would introduce when used as a synchronization primitive instead of a std::atomic (e.g. because - for some reasons - compilers treat std::atomics in a way they don't / can treat this class)
\$\endgroup\$
2
  • \$\begingroup\$ maybe you can override swap (as a friend function) so that it uses an atomic exchange ? \$\endgroup\$
    – v.oddou
    Commented May 30, 2017 at 3:26
  • \$\begingroup\$ @v.oddou: This is an interesting idea. The problem is see, is that it isn't possible to perform the swap atomically without using a lock either way (see e.g. stackoverflow.com/questions/18349688/…). It might however still be worth from an efficiency point of view. \$\endgroup\$
    – MikeMB
    Commented May 30, 2017 at 15:37

1 Answer 1

7
\$\begingroup\$

Difference in default constructor

std::atomic<T>'s default constructor is trivial - yours isn't. If your goal is to simply add copy semantics, I would maintain this same behavior:

CopyableAtomic() = default;

Memory orders

I'm not sure about relaxed here. You probably want to ensure consistent orderings where you use this object, so I would change the load()s to use std::memory_order_acquire and the store() to use std::memory_order_release. That is:

CopyableAtomic& operator=(const CopyableAtomic<T>& other) {
    this->store(
        other.load(std::memory_order_acquire),
        std::memory_order_release);
    return *this;
}

Otherwise, you might get unexpected reorderings. Better to be on the safe side if you're writing a class like this.

Otherwise

This looks perfectly fine to me. With the exception of copying, this is precisely a std::atomic so it should have all the same behavior everywhere - so it should be able to be a straight-forward drop in replacement in all places.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you. Regarding the default constructor I think you are right. I stumbled once over the uninitialized atomics, so I thought, I'd fix that that while I'm at it, but on the other hand, by now I'm almost always writing Foo foo{} by default, so it should not be a problem anymore and it might surprise others. \$\endgroup\$
    – MikeMB
    Commented Dec 9, 2015 at 23:07
  • \$\begingroup\$ Could you show an example, where stronger memory orderings would actually be useful? From my understanding, if I want to make a consistent copy of a class holding such an atomic, I'd have to write my own cpy ctor anyway (which will probably have to take a lock or very carefully specify the order in which the elements are copied). \$\endgroup\$
    – MikeMB
    Commented Dec 10, 2015 at 22:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.