Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can copy elision happen across synchronize-with statements?

Tags:

c++

c++11

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.

like image 793
edA-qa mort-ora-y Avatar asked Jan 19 '12 09:01

edA-qa mort-ora-y


1 Answers

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):

  1. Construct lock.
  2. Construct the temporary user_type with (...) arguments.
  3. Copy-construct the temporary return value of the function, of type user_type using the value from step 2.
  4. Destroy the temporary from step 2.
  5. Destroy lock.
  6. Copy construct the user_type result using the value from step 3.
  7. Destroy the temporary from step 3.
  8. Later on, destroy result.

Naturally, with the multiple copy elision optimizations, it will be just:

  1. Construct lock.
  2. Construct the result object directly with (...).
  3. Destroy lock.
  4. Later on, destroy result.

Note that in both cases the user_type constructor 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:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

  • a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object can be omitted by constructing the automatic object directly into the exception object

  • when a temporary class object that has not been bound to a reference would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

  • when the exception-declaration of an exception handler declares an object of the same type (except for cv-qualification) as the exception object, the copy/move operation can be omitted by treating the exception-declaration as an alias for the exception object if the meaning of the program will be unchanged except for the execution of constructors and destructors for the object declared by the exception-declaration.

Points 1 and 3 collaborate in your example to elide all the copies.

like image 178
rodrigo Avatar answered Nov 15 '22 21:11

rodrigo