In the example below, if we ignore the mutex for a second, copy elision may eliminate the two calls to the copy constructor.
user_type foo()
{
unique_lock lock( global_mutex );
return user_type(...);
}
user_type result = foo();
Now the rules for copy elision don’t mention threading, but I’m wondering whether it should actually happen across such boundaries. In the situation above, the final copy, in the logical abstract machine inter-thread happens after the mutex is released. If however the copies are omitted the result data structure is initialized within the mutex, thus it inter-thread happens before the mutex is released.
I have yet to think of a concrete example how copy elision could truly result in a race condition, but the interference in the memory sequence seems like it might be problem. Can anybody definitively say it can not cause a problem, or can somebody produce an example that can indeed break?
To ensure the answer doesn’t just address a special case, note that copy elision is (according to my reading) still allowed to occur if I have a statement like new (&result)( foo() ). That is, result does not need to be a stack object. user_type itself may also work with data shared between threads.
Answer: I’ve chosen the first answer as the most relevant discussion. Basically since the standard says elision can happen, the programmer just has to be careful when it happens across synchronization bounds. There is no indication of whether this is an intentional or accidental requirement. We’re still lacking in any example showing what could go wrong, so perhaps it isn’t an issue either way.
Threads have nothing to do with it, but the order of constructors/destructors of the lock may affect you.
Looking at the low level steps your code does, with out copy elision, one by one (using the GCC option -fno-elide-constructors):
lock.user_typewith(...)arguments.user_typeusing the value from step 2.lock.user_type resultusing the value from step 3.result.Naturally, with the multiple copy elision optimizations, it will be just:
lock.resultobject directly with(...).lock.result.Note that in both cases the
user_typeconstructor with(...)is protected by the lock. Any other copy constructor or destructor call may not be protected.Afterthoughts:
I think that the most likely place where it can cause problems is in the destructors. That is, if your original object, that constructed with
(...)handles any shared resource differently than its copies, and does something in the destructor that needs the lock, then you have a problem.Naturally, that would mean that your object is badly design in the first place, as copies do not behave as the original object.
Reference:
In the C++11 draft, 12.8.31 (a similar wording without all the “moves” is in C++98:
Points 1 and 3 collaborate in your example to elide all the copies.