📜 ⬆️ ⬇️

(Without) dangerous copy elision



Already a year in my free time I have been drinking something like a mixture of Maven and Spring for C ++. An important part of it is a self-writing system of smart pointers . Why do I need all this - a separate issue. In this article I want to briefly describe how a seemingly useful feature of C ++ made me doubt the common sense of the Standard.

Edited:
I apologize to the Habrasoobshchestvo and Standard. Literally the next day after sending the article, I realized a gross mistake in my thoughts. It is better to read the end of the article right away ... and, yes, to copy elision , it turns out that the article is only indirectly relevant.
')


1. The problem


Smart pointers for the project were made last summer.

Selected index code and explanations
template<typename T_Type, typename T_Holding, typename T_Access> class DReference { . . . IDSharedMemoryHolder *_holder; void retain(IDSharedMemoryHolder *inHolder) { . . . } void release() { . . . } . . . ~DReference() { release(); } template<typename T_OtherType, typename T_OtherHolding, typename T_OtherAccess> DReference( const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> & inLReference) : _holder(NULL), _holding(), _access() { retain(inLReference._holder); } . . . } 

We have strategy structures that implement the object storage logic and the object access logic. We pass their types as template arguments to the smart pointer class. IDSharedMemoryHolder - object memory access interface. By calling the retain () function, the smart pointer begins to own the object (for strong reference ++ ref_count ). By calling release (), the pointer frees the object (for strong reference, --ref_count and deleting an object if ref_count == 0 ).

I deliberately omitted here the things related to dereferencing and with retain on calls from operators. The described problem does not concern these moments.

The work of smart pointers was checked by a series of simple tests: “they created an object associated with the pointer — assigned a pointer to the pointer — looked at the reatin / release to pass correctly”. Tests (which now seems very strange) passed. I translated the code for smart pointers in early January and ... yes, then everything also worked.

The problems started a month ago when it was discovered that the memory controlled by smart pointers was deleted ahead of time.

Let me explain with a specific example:

 DStrongReference<DPlugIn> DPlugInManager::createPlugIn( const DPlugInDescriptor &inDescriptor) { . . . DStrongReference<DPlugIn> thePlugInReference = internalCreatePlugIn(inDescriptor); . . . return thePlugInReference; } ... DStrongReference<DPlugIn> DPlugInManager::internalCreatePlugIn( const DPlugInDescriptor &inDescriptor) { for (IDPlugInStorage *thePlugInStorage : _storages) { if (thePlugInStorage->getPlugInStatus(inDescriptor)) return thePlugInStorage->createPlugIn(inDescriptor); } return DStrongReference<DPlugIn>(); } ... class DPlugInStorageImpl : public IDPlugInStorage { public: virtual ~DPlugInStorageImpl() { } virtual DStrongReference<DPlugIn> createPlugIn( const DPlugInDescriptor &inDescriptor); }; 

When the DPlugInStorageImpl :: createPlugIn (...) method was called, an object was created that was returned via DStrongReference , after which this smart pointer was returned via the DPlugInManager :: internalCreatePlugIn (...) method to the calling context - the DPlugInManager :: createPlugIn method (...) .

So, when the smart pointer returned to the DPlugInManager :: createPlugIn (...) method , thePlugInReference pointed to the remote object. Obviously, it was the wrong number of retain / release-calls. Having spent a lot of nerves with debugger in Eclipse (by the way - it is terrible), I spat, and solved the problem in a simple way - I used a log. I put the output on the calls to the retain and release methods, launched the program ... What did I expect to see? Here is something (pseudocode):

DPlugInStorageImpl :: createPlugIn (...) => RETAIN
DPlugInManager :: internalCreatePlugIn (...), return createPlugIn => RETAIN
DPlugInStorageImpl :: createPlugIn (...), ~ DStrongReference () => RELEASE
DPlugInManager :: createPlugIn (...), thePlugInReference = internalCreatePlugIn (...) => RETAIN
DPlugInManager :: internalCreatePlugIn (...), ~ DStrongReference () => RELEASE

Total: ref_count = 1 for thePlugInReference . Everything had to be clear.

What I actually saw made me do this (0_0) and spend the next hour and a half five minutes doing all sorts of clean-ups, recompiling, rechecking optimization settings, trying to flush stdout, and so on.

DPlugInStorageImpl :: createPlugIn (...) => RETAIN
DPlugInManager :: internalCreatePlugIn (...), ~ DStrongReference () => RELEASE

Desperate to solve the problem in the combat code and already suspecting something extremely wrong, I created a small test project.

2. Test


Test code:

 #include <iostream> #include <stdio.h> class TestClass { private: int _state; public: TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; } TestClass() : _state(1) { std::cout << "Default" << std::endl; } TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; } TestClass(TestClass &inObject0) : _state(3) { std::cout << "Copy" << std::endl; } TestClass(const TestClass &&inObject0) : _state(4) { std::cout << "Const Move" << std::endl; } TestClass(TestClass &&inObject0) : _state(5) { std::cout << "Move" << std::endl; } ~TestClass() { std::cout << "Destroy" << std::endl; } void call() { std::cout << "Call " << _state << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// int main() { TestClass theTestObject = TestClass(); theTestObject.call(); fflush(stdout); return 0; } 

Expected Result:

Default
Const Copy
Call 1
Destroy

The real result:

Default
Call 1
Destroy

That is, copy-designer was not called. And only then did I do what had to be done right away. Googled and found out about copy_elision .

3. The terrible truth


In a nutshell - any C ++ compiler can, without warning and without any flags, ignore the call to the copy constructor and instead, for example, directly copy the full state of the object. At the same time, it is impossible to perform any logic in the process of such copying without hacks . Here it is explicitly stated in the Notes section: " Elizia copying is the only allowed type of optimization that can have observable side effects", "Copy elision is the only possible side effect. "

Optimization is, of course, excellent ... But what if I need to perform some logic in the copy constructor. For example, for smart pointers? And that it is still incomprehensible to me, why it was impossible to allow such an optimization to be carried out with -o1 if there is no logic in the body of the copy constructor? .. Until now, this is not clear to me.

4. Decision


I found two ways to force the compiler to execute logic at the time of constructing class objects:

1) Through compilation flags. Bad way. Compiler-dependent. For example, for g ++ you need to set the -fno-elide-constructors flag, and this will either affect the whole project (which is awful), or you have to use the compiler flags settings in the appropriate places push / pop, which clutters the code and makes it less readable (especially with taking into account what you have to do for each compiler).

2) Via the explicit keyword. This is also a bad way, but in my opinion, this is better than using compilation flags.
The explicit qualifier is needed to prevent implicit instantiation of a class through the casting syntax. That is, in order that instead of MyInt theMyInt = 1, it was necessary to write MyInt theMyInt = MyInt (1) .
If we put this word in front of the copy constructor, we will get a rather amusing ban on implicit type casting - a ban on casting to its type.

So for example, the following

code
 #include <iostream> #include <stdio.h> class TestClass { private: int _state; public: TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; } TestClass() : _state(1) { std::cout << "Default" << std::endl; } explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; } } ~TestClass() { std::cout << "Destroy" << std::endl; } void call() { std::cout << "Call " << _state << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// int main() { TestClass theTestObject = TestClass(); theTestObject.call(); fflush(stdout); return 0; } 


I (g ++ 4.6.1) caused an error:

error: no matching function for call to 'TestClass :: TestClass (TestClass)'

What is even funnier, because of the C ++ syntax, like this: TestClass theTestObject (TestClass ()) won't work either, because this will be considered a declaration of a function pointer and will cause an error:

error: request for member 'call' in 'theTestObject', which is of non-class type 'TestClass (TestClass (*) ())'

Thus, instead of forcing the compiler to execute the copy constructor, we are forbidden to call this constructor.

Fortunately for me, this decision came up. The fact is that by disabling the copy constructor, I forced the compiler to use the specification of the template constructor with the same template arguments as the current class. That is, it was not “casting an object to its type,” but it was “casting to a type that has the same template arguments,” which generates another method, but replaces the copy constructor.

That's what happened
 template<typename T_Type, typename T_Holding, typename T_Access> class DReference { . . . IDSharedMemoryHolder *_holder; void retain(IDSharedMemoryHolder *inHolder) { . . . } void release() { . . . } . . . ~DReference() { release(); } //NB: Workaround for Copy elision explicit DReference( const OwnType &inLReference) : _holder(NULL), _holding(), _access() { // Call for some magic cases retain(inLReference._holder); } template<typename T_OtherType, typename T_OtherHolding, typename T_OtherAccess> DReference( const DReference<T_OtherType, T_OtherHolding, T_OtherAccess> & inLReference) : _holder(NULL), _holding(), _access() { retain(inLReference._holder); } . . . } 



For a test example, an analogue of this crutch would look like this:

Test code and short explanation
 #include <iostream> #include <stdio.h> class TestClass { private: int _state; public: TestClass(int inState) : _state(inState) { std::cout << "State based: " << inState << std::endl; } TestClass() : _state(1) { std::cout << "Default" << std::endl; } explicit TestClass(const TestClass &inObject0) : _state(2) { std::cout << "Const Copy" << std::endl; } template<typename T> TestClass(const T &inObject0) : _state(13) { std::cout << "Template Copy" << std::endl; } ~TestClass() { std::cout << "Destroy" << std::endl; } void call() { std::cout << "Call " << _state << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// int main() { TestClass theTestObject = TestClass(); theTestObject.call(); fflush(stdout); return 0; } 


The same trick. The specification of the template that replaces the copy constructor ... This shows that this is a bad decision, because we out of place used templates. If anyone knows how best - accomplish your goal.


Instead of conclusion


When I talked about copy elision to several acquaintances who had been in C ++ and C-++ development for about three years, they also did this (0_0) just as much as mine. Meanwhile, this optimization can generate behavior that is strange from the programmer’s point of view and cause errors when writing C ++ applications.

I hope this article will be useful to someone and save someone's time.

PS: Write about the missteps I have noticed - I will rule.

Edited:



Commenting on the right, I generally misunderstood the problem. Thanks to everyone, especially Monnoroch for identifying logical errors in the article.

Having written the following test code, I got the correct output:

Template class with copy constructor and output
 /////////////////////////////////////////////////////////////////////////////// #include <iostream> /////////////////////////////////////////////////////////////////////////////// template<typename T_Type> class TestTemplateClass { private: typedef TestTemplateClass<T_Type> OwnType; T_Type _state; public: TestTemplateClass() : _state() { std::cout << "Default constructor" << std::endl; } TestTemplateClass(int inState) : _state(inState) { std::cout << "State constructor" << std::endl; } TestTemplateClass(const OwnType &inValue) { std::cout << "Copy constructor" << std::endl; } template<typename T_OtherType> TestTemplateClass(const TestTemplateClass<T_OtherType> &inValue) { std::cout << "Template-copy constructor" << std::endl; } template<typename T_OtherType> void operator = (const TestTemplateClass<T_OtherType> &inValue) { std::cout << "Operator" << std::endl; } ~TestTemplateClass() { std::cout << "Destructor" << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// TestTemplateClass<int> createFunction() { return TestTemplateClass<int>(); } /////////////////////////////////////////////////////////////////////////////// int main() { TestTemplateClass<int> theReference = createFunction(); std::cout << "Finished" << std::endl; return 0; } /////////////////////////////////////////////////////////////////////////////// 


Output:
 Default constructor Copy constructor Destructor Copy constructor Destructor Finished Destructor 



That is, indeed, the problem was not in copy elision and no hacks are needed.

The real mistake turned out to be banal. Now I am ashamed of the fact that I undertook to write an article without checking everything properly.
The fact is that smart pointers take three template arguments:

 template<typename T_Type, typename T_Holding, typename T_Access> class DReference { . . . 


  1. T_Type is an object type that is controlled by a smart pointer system.
  2. T_Holding - memory ownership strategy.
  3. T_Access is a memory access strategy.


Such an implementation of smart pointers allows for flexible customization of their behavior, but at the same time makes it cumbersome to use (especially given the fact that strategies are also template classes).

Example of a strong pointer declaration:

 DReference<MyType, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > theReference; 


To avoid code clutter, I wanted to use the C ++ 11 standard feature - template-alias . But as it turned out, g ++ 4.6.1 does not support them . Of course, when you write your home pet-project, it’s too lazy to tinker with the environment setup, so I decided to do another workaround and get rid of the argument using inheritance:

 template<typename T_Type> class DStrongReference : public DReference< T_Type, DReferenceStrongHolding<MyType>, DReferenceCachingAccess< MyType > > { . . . 


At the same time, it was necessary to define a bunch of constructors for DStrongReference , which are called from themselves by the corresponding constructors of the base DReference class - after all, constructors are not inherited. And, of course, I missed the copy designer ... In general, the only advice I can give after all these adventures is that you need to be very careful when using templates in order not to get into such a stupid situation that I got into.

PS: Here is a test that uses inheritance to replace template-alias (thanks to ToSHiC for a good advice to pass this to output):

Test imitation of template-alias
 /////////////////////////////////////////////////////////////////////////////// #include <iostream> /////////////////////////////////////////////////////////////////////////////// template<typename T_Type, typename T_Strategy> class TestTemplateClass { private: typedef TestTemplateClass<T_Type, T_Strategy> OwnType; T_Type _state; T_Strategy _strategy; public: TestTemplateClass() : _state(), _strategy() { std::cout << "Default constructor: " << this << std::endl; } TestTemplateClass(int inState) : _state(inState), _strategy() { std::cout << "State constructor: " << this << std::endl; } TestTemplateClass(const OwnType &inValue) : _state(), _strategy() { std::cout << "Copy constructor: " << this << " from " << &inValue << std::endl; } template<typename T_OtherType, typename T_OtherStrategy> TestTemplateClass( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) : _state(), _strategy() { std::cout << "Template-copy constructor: " << this << std::endl; } void operator = (const OwnType &inValue) { std::cout << "Assigning: " << this << " from " << inValue << std::endl; } template<typename T_OtherType, typename T_OtherStrategy> void operator = ( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) { std::cout << "Assigning: " << this << " from " << &inValue << std::endl; } ~TestTemplateClass() { std::cout << "Destructor: " << this << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// template<typename T_Type> class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> { private: //- Types typedef TestTemplateClassIntStrategy<T_Type> OwnType; typedef TestTemplateClass<T_Type, int> ParentType; public: TestTemplateClassIntStrategy() : ParentType() { } TestTemplateClassIntStrategy(int inState) : ParentType(inState) { } TestTemplateClassIntStrategy(const OwnType &inValue) : ParentType(inValue) { } template<typename T_OtherType, typename T_OtherStrategy> TestTemplateClassIntStrategy( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) : ParentType(inValue) { } //- Operators void operator = (const OwnType &inValue) { ParentType::operator =(inValue); } template<typename T_OtherType, typename T_OtherStrategy> void operator = ( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) { ParentType::operator =(inValue); } }; /////////////////////////////////////////////////////////////////////////////// TestTemplateClassIntStrategy<int> createFunction() { return TestTemplateClassIntStrategy<int>(); } int main() { TestTemplateClassIntStrategy<int> theReference = createFunction(); std::cout << "Finished" << std::endl; return 0; } 


Output:
 Default constructor: 0x28fed8 Copy constructor: 0x28ff08 from 0x28fed8 Destructor: 0x28fed8 Copy constructor: 0x28ff00 from 0x28ff08 Destructor: 0x28ff08 Finished Destructor: 0x28ff00 



Call the assignment operator
 . . . int main() { TestTemplateClassIntStrategy<int> theReference; theReference = createFunction(); std::cout << "Finished" << std::endl; return 0; } 


 Default constructor: 0x28ff00 Default constructor: 0x28fed8 Copy constructor: 0x28ff08 from 0x28fed8 Destructor: 0x28fed8 Assigning: 0x28ff00 from 0x28ff08 Destructor: 0x28ff08 Finished Destructor: 0x28ff00 



An important disadvantage of this method: if you define a strong-pointer and a weak-pointer in this way, they will be completely different types (not even associated with one template class) and will not leave them to assign one directly to another at the time of initialization.

<Edited # 2>

Again, hurried to say something. I understood at night. It will come out after all ... These classes have one common patterned ancestor.
That is, if the template constructor from an arbitrary DReference is described in the successor (which imitates template-alias), everything will be fine in the following code:

Code
 DStrongReference<Type> theStrongReference; //           .  : // // template<typename Type, typename Owning, typename Holding> // DWeakReference::DWeakReference(const DReference<Type, Owning, Holding> &ref) : Parent(ref) { } // //       ,  DStrongReference  DReference. // DWeakReference<Type> theWeakReference = theStrongReference; 



Test code for two classes organized this way:

template-alias through inheritance: two pseudo-alias
 //============================================================================ // Name : demiurg_application_example.cpp // Author : // Version : // Copyright : Your copyright notice // Description : Hello World in C++, Ansi-style //============================================================================ /////////////////////////////////////////////////////////////////////////////// #include <iostream> /////////////////////////////////////////////////////////////////////////////// template<typename T_Type, typename T_Strategy> class TestTemplateClass { private: typedef TestTemplateClass<T_Type, T_Strategy> OwnType; T_Type _state; T_Strategy _strategy; public: TestTemplateClass() : _state(), _strategy() { std::cout << "Default constructor: " << this << std::endl; } TestTemplateClass(int inState) : _state(inState), _strategy() { std::cout << "State constructor: " << this << std::endl; } TestTemplateClass(const OwnType &inValue) : _state(), _strategy() { std::cout << "Copy constructor: " << this << " from " << &inValue << std::endl; } template<typename T_OtherType, typename T_OtherStrategy> TestTemplateClass( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) : _state(), _strategy() { std::cout << "Template-copy constructor: " << this << std::endl; } void operator = (const OwnType &inValue) { std::cout << "Assigning: " << this << " from " << &inValue << std::endl; } template<typename T_OtherType, typename T_OtherStrategy> void operator = ( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) { std::cout << "Assigning: " << this << " from " << &inValue << std::endl; } ~TestTemplateClass() { std::cout << "Destructor: " << this << std::endl; } }; /////////////////////////////////////////////////////////////////////////////// //- Integer strategy template<typename T_Type> class TestTemplateClassIntStrategy : public TestTemplateClass<T_Type, int> { private: //- Types typedef TestTemplateClassIntStrategy<T_Type> OwnType; typedef TestTemplateClass<T_Type, int> ParentType; public: TestTemplateClassIntStrategy() : ParentType() { } TestTemplateClassIntStrategy(int inState) : ParentType(inState) { } TestTemplateClassIntStrategy(const OwnType &inValue) : ParentType(inValue) { } template<typename T_OtherType, typename T_OtherStrategy> TestTemplateClassIntStrategy( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) : ParentType(inValue) { } //- Operators void operator = (const OwnType &inValue) { ParentType::operator =(inValue); } template<typename T_OtherType, typename T_OtherStrategy> void operator = ( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) { ParentType::operator =(inValue); } }; //- Boolean strategy template<typename T_Type> class TestTemplateClassBoolStrategy : public TestTemplateClass<T_Type, bool> { private: //- Types typedef TestTemplateClassBoolStrategy<T_Type> OwnType; typedef TestTemplateClass<T_Type, bool> ParentType; public: TestTemplateClassBoolStrategy() : ParentType() { } TestTemplateClassBoolStrategy(int inState) : ParentType(inState) { } TestTemplateClassBoolStrategy(const OwnType &inValue) : ParentType(inValue) { } template<typename T_OtherType, typename T_OtherStrategy> TestTemplateClassBoolStrategy( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) : ParentType(inValue) { } //- Operators void operator = (const OwnType &inValue) { ParentType::operator =(inValue); } template<typename T_OtherType, typename T_OtherStrategy> void operator = ( const TestTemplateClass<T_OtherType, T_OtherStrategy> &inValue) { ParentType::operator =(inValue); } }; /////////////////////////////////////////////////////////////////////////////// TestTemplateClassBoolStrategy<int> createFunction() { return TestTemplateClassBoolStrategy<int>(); } int main() { TestTemplateClassIntStrategy<int> theReference; theReference = createFunction(); std::cout << "Finished" << std::endl; return 0; } 


Output:
 Default constructor: 0x28fed8 Copy constructor: 0x28ff08 from 0x28fed8 Destructor: 0x28fed8 Copy constructor: 0x28ff00 from 0x28ff08 Destructor: 0x28ff08 Finished Destructor: 0x28ff00 



In general, everything works

</ Edited number 2>

Thanks 1eqinfinity , Torvald3d for pointing spelling errors.

Source: https://habr.com/ru/post/252885/


All Articles