(No) dangerous copy elision



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

Edited:
I apologize to the habrasociety and the Standard. Just the day after the article was posted, I realized a gross error in my thoughts. It is better to read immediately the end of the article ... and, yes, copy elision , it turns out, the article is only indirectly related.



1. The problem


Smart pointers for the project were made last summer.

Selected Pointer Code and Explanations
template
class DReference {
. . .
	IDSharedMemoryHolder *_holder;
	void retain(IDSharedMemoryHolder *inHolder) { . . . }
	void release() { . . . }
 . . .
	~DReference() { release(); }
	template
	DReference(
			const DReference &
					inLReference) : _holder(NULL), _holding(), _access()
	{
		retain(inLReference._holder);
	}
. . . 
}

We have strategy structures that implement the logic of object storage and the logic of access to the object. 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 a ++ ref_count ). By calling release (), the pointer releases the object (for strong reference --ref_count and deleting the object if ref_count == 0 ).

I intentionally omitted things related to dereferencing and retain here on operator calls. The described problem of these points does not concern.

The work of smart pointers was tested by a number of simple tests: "created an object associated with the pointer - assigned a pointer to the pointer - looked to make reatin / release go right." Tests (which now seems very strange) passed. I translated the code to smart pointers in early January and ... yes, then everything worked too.

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

I will explain with a specific example:

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

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

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

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 forthePlugInReference . Everything had to be clear.

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

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


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

2. Test


Test code:

#include 
#include 
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


Real result:

Default
Call 1
Destroy


That is, the copy constructor was not called. And only then I did what needed to be done right away. Googled and found out about copy_elision .

3. The terrible truth


В двух словах — любой компилятор С++ может без предупреждения и каких-либо флагов игнорировать вызов copy-конструктора и вместо этого, например, напрямую копировать полное состояние объекта. При этом выполнить какую-либо логику в процессе такого копирования без хаков просто так нельзя. Вот тут в разделе Notes прямо сказано: "Элизия копирования — единственный разрешённый вид оптимизации, который может иметь наблюдаемые побочные эффекты", «Copy elision is the only allowed form of optimization that can change the observable side-effects».

Оптимизация — это, конечно, отлично… Но что если мне нужноexecute any logic in the copy constructor. For example, for smart pointers? And it’s still not clear to me why it was impossible to allow similar optimization with -o1 if there was 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 entire project (which is terrible), or you will have to use compiler flag settings in the appropriate push / pop places, which clutters the code and makes it less readable (especially with taking into account that this will have to be done for each compiler).

2) Through 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 prohibit the implicit creation of instances of a class through type casting syntax. That is, in order to instead of MyInt theMyInt = 1, it was necessary to write MyInt theMyInt = MyInt (1) .
If you put this word in front of the copy-constructor, we get a rather funny ban on implicit type conversion - a ban on casting to its type.

So, for example, the following

the code
#include 
#include 
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's even funnier, because of C ++ syntax features like this: TestClass theTestObject (TestClass ()) writing will not work either, because it will be considered as declaring 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 forbade calling this constructor.

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

That's what happened
template
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
	DReference(
			const DReference &
					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 
#include 
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
	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 chip. The template specification, replacing the copy constructor ... It can be seen that this is a bad solution, because we used templates out of place. If anyone knows how best - unsubscribe.


Instead of a conclusion


When I talked about copy elision to several friends who had been in C ++ and about-C ++ for three years, they also did like this (0_0) they were no less surprised than mine. Meanwhile, this optimization can give rise to behavior that is strange from the point of view of the programmer, and cause errors when writing C ++ applications.

I hope this article is useful to someone and saves someone’s time.

PS: Write about the noticed oversights - I will edit.

Edited by:



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

By writing the following test code, I got the correct output:

Template class with copy constructor and output
///////////////////////////////////////////////////////////////////////////////
#include 
///////////////////////////////////////////////////////////////////////////////
template
class TestTemplateClass {
private:
	typedef TestTemplateClass 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
    TestTemplateClass(const TestTemplateClass &inValue) {
        std::cout << "Template-copy constructor" << std::endl;
    }
    template
    void operator = (const TestTemplateClass &inValue) {
        std::cout << "Operator" << std::endl;
    }
    ~TestTemplateClass() {
        std::cout << "Destructor" << std::endl;
    }
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClass createFunction() {
    return TestTemplateClass();
}
///////////////////////////////////////////////////////////////////////////////
int main() {
    TestTemplateClass 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 were needed.

The real mistake turned out to be commonplace. Now I am ashamed that I undertook to write an article, without checking everything as it should.
The fact is that smart pointers accept three template arguments:

template
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.


This implementation of smart pointers allows you to make customizing their behavior flexible, but at the same time makes their use cumbersome (especially since strategies are also template classes).

Example of declaring a strong pointer:

DReference, DReferenceCachingAccess< MyType > > theReference;


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

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


In this case, it was necessary to define a bunch of constructors for DStrongReference , calling themselves the corresponding constructors of the DReference base class - because the constructors are not inherited. And, of course, I missed the copy constructor ... In general, the only advice I can give after all these adventures is to be very careful when using templates so as 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 ToSHiC for the good advice of passing this to output):

Test simulation template-alias
///////////////////////////////////////////////////////////////////////////////
#include 
///////////////////////////////////////////////////////////////////////////////
template
class TestTemplateClass {
private:
	typedef TestTemplateClass 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
	TestTemplateClass(
			const TestTemplateClass &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
	void operator = (
			const TestTemplateClass &inValue)
	{
		std::cout << "Assigning: " << this << " from "
				<< &inValue << std::endl;
	}
	~TestTemplateClass() {
		std::cout << "Destructor: " << this << std::endl;
	}
};
///////////////////////////////////////////////////////////////////////////////
template
class TestTemplateClassIntStrategy : public TestTemplateClass {
private:
	//- Types
	typedef TestTemplateClassIntStrategy OwnType;
	typedef TestTemplateClass ParentType;
public:
	TestTemplateClassIntStrategy() : ParentType() { }
	TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }
	TestTemplateClassIntStrategy(const OwnType &inValue)
		: ParentType(inValue) { }
	template
	TestTemplateClassIntStrategy(
			const TestTemplateClass &inValue)
		: ParentType(inValue) { }
	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}
	template
	void operator = (
			const TestTemplateClass &inValue)
	{
		ParentType::operator =(inValue);
	}
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClassIntStrategy createFunction() {
	return TestTemplateClassIntStrategy();
}
int main() {
	TestTemplateClassIntStrategy 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



Assignment Statement Call
. . .
int main() {
	TestTemplateClassIntStrategy 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 minus of this method: if you define strong-pointer and weak-pointer in this way, they will be of completely different types (not even associated with one template class) and it will not work to assign them one to another directly at the time of initialization.

<Edited No. 2>

Again, I hurried to affirm something. I got it at night. It will come out after all ... These classes have one common template 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:

The code
DStrongReference theStrongReference;
// В следующей строке будет вызван шаблонный конструктор от базового шаблонного класса. Вот этот:
//
// template
// DWeakReference::DWeakReference(const DReference &ref) : Parent(ref) { }
//
// Этот конструктор будет вызван за счёт того, что DStrongReference наследует DReference.
//
DWeakReference theWeakReference = theStrongReference;



Test code for two classes organized in this way:

template-alias via inheritance: two pseudo-alias
//============================================================================
// Name        : demiurg_application_example.cpp
// Author      : 
// Version     :
// Copyright   : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================
///////////////////////////////////////////////////////////////////////////////
#include 
///////////////////////////////////////////////////////////////////////////////
template
class TestTemplateClass {
private:
	typedef TestTemplateClass 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
	TestTemplateClass(
			const TestTemplateClass &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
	void operator = (
			const TestTemplateClass &inValue)
	{
		std::cout << "Assigning: " << this << " from "
				<< &inValue << std::endl;
	}
	~TestTemplateClass() {
		std::cout << "Destructor: " << this << std::endl;
	}
};
///////////////////////////////////////////////////////////////////////////////
//- Integer strategy
template
class TestTemplateClassIntStrategy : public TestTemplateClass {
private:
	//- Types
	typedef TestTemplateClassIntStrategy OwnType;
	typedef TestTemplateClass ParentType;
public:
	TestTemplateClassIntStrategy() : ParentType() { }
	TestTemplateClassIntStrategy(int inState) : ParentType(inState) { }
	TestTemplateClassIntStrategy(const OwnType &inValue)
		: ParentType(inValue) { }
	template
	TestTemplateClassIntStrategy(
			const TestTemplateClass &inValue)
		: ParentType(inValue) { }
	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}
	template
	void operator = (
			const TestTemplateClass &inValue)
	{
		ParentType::operator =(inValue);
	}
};
//- Boolean strategy
template
class TestTemplateClassBoolStrategy : public TestTemplateClass {
private:
	//- Types
	typedef TestTemplateClassBoolStrategy OwnType;
	typedef TestTemplateClass ParentType;
public:
	TestTemplateClassBoolStrategy() : ParentType() { }
	TestTemplateClassBoolStrategy(int inState) : ParentType(inState) { }
	TestTemplateClassBoolStrategy(const OwnType &inValue)
		: ParentType(inValue) { }
	template
	TestTemplateClassBoolStrategy(
			const TestTemplateClass &inValue)
		: ParentType(inValue) { }
	//- Operators
	void operator = (const OwnType &inValue) {
		ParentType::operator =(inValue);
	}
	template
	void operator = (
			const TestTemplateClass &inValue)
	{
		ParentType::operator =(inValue);
	}
};
///////////////////////////////////////////////////////////////////////////////
TestTemplateClassBoolStrategy createFunction() {
	return TestTemplateClassBoolStrategy();
}
int main() {
	TestTemplateClassIntStrategy 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 Thanks 1eqinfinity , Torvald3d for pointing out spelling errors.




Also popular now: