I have a class with an atomic member and i want to write a copy constructor:
struct Foo { std::atomic mInt; Foo() {} Foo(const Foo& pOther) { std::atomic_store(mInt, std::atomic_load(pOther.mInt, memory_order_relaxed), memory_order_relaxed); } };
But i don't know which ordering i must use, because i don't know where and when this copy constructor will be called.
Can i use relaxed
ordering for copy constructor and assignment operator?
No, if you don't know how it will be used, you should use memory_order_seq_cst
to be safe. If you use memory_order_relaxed
, you could run into issues with instructions being reordered.
You only need a stronger memory ordering than memory_order_relaxed
, if your copy operation is supposed to synchronize with other operations on a different thread.
However, this is almost never the case, as a thread safe copy constructor will almost always require some external synchronization or an extra mutex anyway.
The std::atomic
template deletes its copy-constructor, because atomics are for shared state, so copying them to another atomic is usually not what you want.
Deleting the copy-constructor forces users of your class to think about what they're doing, and document that they're doing an atomic-load of one value and then passing that copy to somewhere else. (e.g. atomic var1 (var2.load())
). See C++11: write move constructor with atomic member?
The constructor for std::atomic
is not itself atomic, so it makes little sense to worry about the ordering of a store to it in your constructor (unless your constructor called a bunch of other functions and put the address of mInt
somewhere that another thread could get it...)
Even better, use the copied value as an initializer, instead of doing an atomic store at all. (See also Nonlocking Way to Copy Atomics in Copy Constructor).
I think the only way this could be a problem is if you were doing something that's already undefined-behaviour, like using placement-new
to construct a new Foo
object in an already-shared location that could be read/written by other threads as you did this. This is obviously insane, so don't do that.
Having your class's memory-ordering behaviour match std::atomic
's constructor (i.e. none for storing the initializer) seems like a good idea.
Only the caller knows whether sequential-consistency is required for the load from the source operand. Thus, you should let the caller choose by accepting a memory-order argument, with default=seq_cst (for consistency with std::atomic
, not because that's what anyone is likely to want for this case). And yes, this is legal C++: copy constructor with default arguments
#include struct Foo { std::atomic mInt; Foo() {} Foo(const Foo& pOther, std::memory_order order = std::memory_order_seq_cst) : mInt(pOther.mInt.load(order)) {} };
This compiles the way I expected: with ordering for the load but no ordering for the store. (e.g. looking at the asm output for ARM64 shows that the load uses ldar
to do an acquire-load, but the store is just a simple str
).
I tested it with this caller (Godbolt compiler explorer) which constructs one on the stack and then passes its address to a non-inline function that might make that address available to other threads. So it can't optimize away.
void extf(Foo &); // non-inline function void test(const Foo *p) { Foo tmp(*p); extf(tmp); }
Whatever extf()
does to make the address available to other threads should use a release-store, which ensures that any other thread which sees that address will see a properly-constructed Foo
. This is a normal requirement, and is why it's totally fine that the initializer isn't even atomic.
Note that it's impossible to do a move between two different memory locations as a single atomic operation (in C++11 or on any hardware I'm aware of), so it's unlikely that a strong ordering will be useful.
Even defining whether such a move is atomic or not is problematic, because atomicity only exists in the eyes of an observer. Since it's impossible to observe two memory locations at the same time it's a meaningless concept. (Unless they're adjacent and you can get them both with a single atomic load).