Designer skip is a pretty nice optimization in terms of speed. But is she safe? Let's figure it out. First, a little information for those who are not yet aware.
Copy elision (copy skip) - optimization, which consists in the fact that the compiler can get rid of the call "extra" copy constructors.
For example:
class M { public: M() { cout << "M()" << endl; } M(const M &obj) { cout << "M(const M &obj)" << endl; } }; M func() { M m1; return m1; } int _tmain(int argc, _TCHAR* argv[]) { M m2 = func(); Sleep(-1); return 0; }
What does this code do? Let me suggest:
')
1) A temporary object m1 of class M is created in the body func ();
2) The copy constructor is called to place a copy of m1 in the return value, because the function must provide a copy of the temporary object that will be destroyed as soon as the function body is exited;
3) The destructor is called for the temporary object m1;
4) A copy constructor is called for m2, which constructs m2 based on the object returned by func ();
5) The temporary object destructor is called, which is returned by func (), since it is no longer needed.
In fact, the result of this code will be different! I run the program in Visual Studio 2013 Release, I see:
M ()The optimizer removed the call for two whole copy constructors and two destructors. I repeat the experiment, it is quite possible that an elementary drop occurred, by analogy with empty cycles and ballast variables. I bring the code to the form close to the battle. For clarity, I do not define a moving assignment operator and a moving constructor:
size_t I = 0;// . class M { private: int i; public: M(int Value = 0) : i{ Value } { ++I; cout << "M(" << Value << ")" << endl; } M(const M &obj) : i{ obj.i } { ++I; cout << "M(const M &obj)" << endl; } M &operator=(const M &obj) { if (this != &obj) { i = obj.i; cout << "M &operator=(const M &obj)" << endl; } return *this; } ~M() { ++I; cout << "~M()" << endl; } }; M func(int Value) { if (Value > 0) { M m1{ 100 }; return m1; } else { M m1{ -100 }; return m1; } } int _tmain(int argc, _TCHAR* argv[]) { M m2 = func(1); cout << endl; M m3 = func(-1); cout << endl; cout << I << endl; Sleep(-1); return 0; }
What should happen as a result of this program? Logically, the following should occur:
1) A temporary object of class M should be created in one of the logical branches func () - which means calling a constructor with a parameter for the temporary object being created;
2) A temporary object that is created in the function body should be copied, so that when exiting the function there is a copy of this temporary object;
3) A call should be made for the destructor of the local object m1;
4) A call should be made to the copy constructor for m2, which takes as its argument an object that returns func () (a copy of the local object);
5) There should be a call to the temporary object destructor, which the function returned for transfer to the copy constructor m2;
...) For m3, everything is the same.
What do we expect? We expect five calls ++ I when executing M m2 = func (); And five more calls ++ I when executing M m3 = func ();
The result of the program:
M (100)
M (const M & obj)
~ M ()
M (-100)
M (const M & obj)
~ M ()
6
What do we see here? And we see the pain. The compiler ignored the fact that M constructors and destructors have global side effects. The smart compiler threw out of our logical chain:
- creating a copy m1 to transfer a copy of the local object as a result of the function;
- call the destructor of the local object m1 (which is logical after the first step).
The local object destructor was called only after calling the m2 / m3 copy constructor.
As a result - I changed not by 10, but by 6.
And now let's introduce a small edit - let's make func () not return a copy of M, but the & M link. The logic prompts that it is impossible to do so - the link to the local object becomes incorrect immediately after exiting the function. It is obvious.
But since we have a “smart” compiler that postpones the call to the local object's destructor until the copy constructor completes, then why not take a chance? Perhaps the compiler will postpone the call to the local object's destructor, the link to which is used? It would be very good and right. That would make sense. We try:
M &func(int Value)// , . { if (Value > 0) { M m1{ 100 }; return m1; } else { M m1{ -100 }; return m1; } }
The result of the program:
M (100)
~ M ()
M (M & obj)
M (-100)
~ M ()
M (M & obj)
6
What do we see? The local object m1 destructor is now called immediately after exiting the function. The smart compiler does the opposite. Link becomes a bat. Based on the reference to the destroyed object, another object is created. The program does not cause an exception only by a miracle. If the class had some kind of dynamic data or implemented the movement semantics, we would get very unpleasant and incomprehensible stealth errors. In a big project is a guarantee of long and exciting adventures.
Such is Copy elision ...
The compiler ignores global constructor side effects. Yes, this behavior is defined in the Standard - the use of copy elision in the presence of global side effects in copy constructors / constructors. As a result, the program functions absolutely incorrectly, and the potential number of problems in a large project increases to indecency.
In fact, there are some more interesting points that stem from this behavior. For those who are interested - I recommend playing with different compilation settings and moving constructors.
After such things, I generally cease to understand what is happening in the code that I am writing. The main rule that can be derived from these examples is:
- Never, absolutely, never allow global side effects in constructors.
Considering the fact that the design logic can vary in each particular code segment, it is difficult to even imagine what problems a seemingly correct code can cause due to “skipping a copy”.
Ps. Please express all criticism regarding the article. This is my first article, I understand that I am not setting out quite smoothly and transparently, but I wanted to help those who might be faced with this behavior of the compiler. Himself for a long time dealt with this.