authentec authentec - 2 months ago 5
C++ Question

Am I guaranteed a single pair of constructor and destructor calls when returning an object initialized with braced init list?

Take the following class for example

#include <iostream>

using namespace std;

class A{
private:
int a_;
int b_;

A(const A&) = delete;
A& operator=(const A&) = delete;
A(A&&) = delete;
A& operator=(A&&) = delete;

public:
A(int a, int b) : a_{a}, b_{b}{cout<<"constructed\n";}

void ephemeral() const{cout<<"operation\n";}

~A(){cout<<"destructed\n";}

};

A make_A(int a, int b){
return {a, b};
}

int main(){
make_A(1, 2).ephemeral();
return 0;
}


It gives the expected result.

The object is constructed, the operation is performed, then it is destructed.

However, I'm concerned whether this is guaranteed. My main concern is whether I could be seeing any effects I'm unaware of due to freedom given to the compiler by the standard.

I don't think copy-elision is a factor here because all move and copy constructors are declared deleted so how could they be called?

The only constructor being called is the one that takes two integers. Can I be sure that this will behave consistently across compilers, platforms, and optimization levels?

I suspect that the answer is 'yes' but there could be subtleties.

Answer

When you return {a,b}; you directly construct the return value.

No temporary, logical or otherwise, is created. No elision occurs.

This return value is available in the returned context in main. You can call its .ephemeral() operation. At the end of the full-expression, it goes out of scope, unless you "store" it in a A const& (reference lifetime extension kicks in) or in a A&& (ditto) or in an auto const& or auto&& variable like this:

auto&& a = make_A(1, 2);
a.ephemeral();

Still, in the above case, no copy occurs.

None of these actions can cause a copy under the standard.

You are correct in some cases copy construction can be elided out of existence. Elision is when two objects have their identity and lifetimes merged. So if make_A read:

A make_A(int a, int b){
  A r{a,b};
  return r;
}

r could be elided into the return value. Here, however, the compiler would demand that A(A const&) or A(A&&) be defined, so it would not compile with your A. In practice, once it checked that they are defined, it wouldn't call them because the r within make_A would be elided to be the same object as the return value of make_A.

Similarly,

A a = make_A(1,2);

the temporary returned by make_A is elided to be the same as the named variable a. Elision is transient, so this could also elide together a variable within make_A. In this case, you also need A(A&&) or A(A const&) to exist.

By deleting the move/copy ctors, they cannot be called, so the object cannot be copied. Only one destructor can be called per constructor (barring manual construction or destruction).

If the code tries to call them, it will generate an error at compile time.


In C++17 you can even return A(a,b); and a similar guarantee occurs.

You can also A a = make_A(1,2); and a similar guarantee occurs.

This is described as "guaranteed elision", but rather it turns some operations into "descriptions of how to construct something".

So there are cases where you'll be able to do things that required move or copy ctors in C++03 or C++11 or C++14 but in C++17 now do something similar to "elision" and no longer require the move or copy ctors.