How to Enforce Copy Elision, Why It Won't Work with Deleted Copy Constructor

How to enforce copy elision, why it won't work with deleted copy constructor?

Until C++17 copy elision is an optimization the compiler is not required to do, so classes must be copyable since the compiler might want to copy (even if it actually does not). In C++17 copy elision will be guaranteed in many cases and then classes won't need copy ctors.

See also:

http://en.cppreference.com/w/cpp/language/copy_elision

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0135r0.html

https://herbsutter.com/2016/06/30/trip-report-summer-iso-c-standards-meeting-oulu/
(the bit about "Guaranteed copy elision")

You could perhaps use the old trick of declaring the copy constructor in your class but not actually implement it? That should please the compiler as long as it does not actually invoke the copy ctor. I didn't test that, but I believe it should work for your case until C++17 arrives.

Why disabling copy elision for std::atomic doesn't work using C++17?

C++17 doesn't simply say that the previously optional return value optimizations are now mandatory. The actual description of the language changed so that there is no creation of a temporary object anymore in the first place.

So, since C++17, there is no constructor call that could be elided anymore. Hence it makes sense that -fno-elide-constructors doesn't add any temporary creation. That would be against the language rules.

Before C++17 the language describes that a temporary object is created from which the variable is initialized and then adds that a compiler is allowed to elide this temporary. Therefore, whether -fno-elide-constructors is used or not, the compiler is behaving standard compliant by eliding or not eliding the temporary copy.

While doing copy-elision, the compiler doesn't consider the copy constructor in overload resolution, when the move constructor is deleted. Why?

This is nothing specific to do with copy elision or constructors; it is just overload resolution.

If we have a pair of overloads:

void f( T&& rv );
void f( const T& lv );

then the overload resolution rules say that f( T{} ) is a better match for f(T&&).

Copy elision can elide a copy or move, but only when the code is well-defined ( even if the compiler chooses not to implement copy elision). Your code is not well-defined, because it specifies to call a deleted function.

Why is the copy constructor not called when the function returns?

With the above set-up it is quite likely that the compiler elides the copy constructor and, instead, directly constructs the temporary temp in the location where the return value will be expected. Copy elision is explicitly allowed even if the copy constructor has side effects. However, even if the copy is elided, the copy or move constructor still has to be accessible, i.e., the potential of copy elision doesn't relax the rules on the corresponding constructor to be accessible.

If you feel you absolutely want to have a copy constructor called, you can force copy construction, e.g., by passing the result through an identity function:

template <typename T>
T const& identity(T const& object) {
return object;
}
// ...
return identity(temp);

Normally, you'd want the copy constructor to be elided, however.

Move constructor vs Copy elision

There are plenty of cases where the move constructor will still get called and copy elision is not being used:

// inserting existing objects into a container
MyObject myobject;
std::vector<MyObject> myvector;
myvector.push_back(std::move(myobject));

// inserting temporary objects into a container
myvector.push_back(MyObject());

// swapping
MyObject other;
std::swap(myobject, other);

// calling functions with existing objects
void foo(MyObject x);

foo(std::move(myobject));

... and many more.

The only instance where there is mandatory copy elision (since C++17) is when constructing values from the result of a function call or a constructor. In such cases, the compiler isn't even allowed to use the move constructor. For example:

MyObject bar() {
return MyObject();
}

void example() {
MyObject x = bar(); // copy elision here
MyObject y = MyObject(); // also here
}

In general, the purpose of copy elision is not to eliminate move construction alltogether, but to avoid unnecessary constructions when initializing variables from prvalues.


See cppreference on Copy Elision.

Forcing the copy constructor

If you have a user-declared (manually declared) copy ctor, the move ctor will not be declared implicitly. Just leave out the move ctor, and it won't take part in overload resolution (i.e. don't define it as deleted, just leave out the entire declaration).

After CWG DR 1402, the explanation is slightly different: a move ctor will always be declared even if there's a user-declared copy ctor, but it will be implicitly defined as deleted. And there's a special case for this deletion that makes the move ctor not take part in overload resolution. Note: If you explicitly delete the move ctor, that still means "if this function is selected by overload resolution, the program is ill-formed". The special case applies when the move ctor is defaulted (explicitly or implicitly), and when this leads to the move ctor being defined as deleted (as the implicit definition supplied for a defaulted function).

#include <iostream>

struct loud
{
loud() { std::cout << "default ctor\n"; }
loud(loud const&) { std::cout << "copy ctor\n"; }
loud(loud&&) { std::cout << "move ctor\n"; }
~loud() { std::cout << "dtor\n"; }
};

struct foo
{
loud l;
foo() = default;
foo(foo const& p) : l(p.l) { /*..*/ }; // or `= default;`
// don't add a move ctor, not even deleted!
};

foo make_foo()
{
return {{}};
}

int main()
{
auto x = make_foo();
}

Watch out for copy elision (e.g. use -fno-elide-constructors). Output:


default ctor
copy ctor
dtor
dtor

Live example

What are copy elision and return value optimization?

Introduction

For a technical overview - skip to this answer.

For common cases where copy elision occurs - skip to this answer.

Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).

It's the only form of optimization that elides (ha!) the as-if rule - copy elision can be applied even if copying/moving the object has side-effects.

The following example taken from Wikipedia:

struct C {
C() {}
C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
return C();
}

int main() {
std::cout << "Hello World!\n";
C obj = f();
}

Depending on the compiler & settings, the following outputs are all valid:

Hello World!

A copy was made.

A copy was made.



Hello World!

A copy was made.



Hello World!

This also means fewer objects can be created, so you also can't rely on a specific number of destructors being called. You shouldn't have critical logic inside copy/move-constructors or destructors, as you can't rely on them being called.

If a call to a copy or move constructor is elided, that constructor must still exist and must be accessible. This ensures that copy elision does not allow copying objects which are not normally copyable, e.g. because they have a private or deleted copy/move constructor.

C++17: As of C++17, Copy Elision is guaranteed when an object is returned directly:

struct C {
C() {}
C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
return C(); //Definitely performs copy elision
}
C g() {
C c;
return c; //Maybe performs copy elision
}

int main() {
std::cout << "Hello World!\n";
C obj = f(); //Copy constructor isn't called
}


Related Topics



Leave a reply



Submit