Does the Behavior of Guaranteed Copy Elision Depend on Existence of User-Defined Copy Constructor

Does the behavior of guaranteed copy elision depend on existence of user-defined copy constructor?

Quoting from C++17 Working Draft §15.2 Temporary Objects Paragraph 3 (https://timsong-cpp.github.io/cppwp/class.temporary#3):

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. ... [ Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note]

In your case, when I made both copy and move constructors defaulted:

S(const S &) = default;
S(S &&) = default;

assertion failed as well with GCC and Clang. Note that implicitly-defined constructors are trivial.

With guaranteed copy elision, why does the class need to be fully defined?

The rule lies in [basic.lval]/9:

Unless otherwise indicated ([dcl.type.simple]), a prvalue shall always have complete type or the void type; ...

Copy/move elision requires explicit definition of copy/move constructors

Because this is a special case where copy elision may not apply.

Quoted from [class.temporary] paragraph 3:

When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object). [ Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note ]

Behavior of constructors when copy constructor is defined by user

This is not a problem specifically related to copy constructors; non-const lvalues references cannot bind to temporaries.

The following code would have compiled:

fruit f1;
fruit f2(f1);

because f1 is not a temporary when you pass it in.

Still, you want temporaries to work, so write your copy constructor like this:

fruit(const fruit& f) {
i = f.i + 1;
}

Your cases 1 and 3 succeed because the compiler is able to generate its own "default" copy constructor having seen that you did not provide one, which — unlike you — it does correctly. :-)

Can initializing an object with a prvalue function result call the move constructor?

Per [class.temporary]/3 RVO on the return value of a function has an exception.

If the type has at least one eligible copy or move constructor, all of them trivial, and if it has a trivial or deleted destructor, then the compiler is allowed not to apply RVO and create a temporary in which the function return value is held before it is copied/moved to initialize the variable.

test_rvo as shown satisfies these requirements and therefore RVO is not mandatory. With a deleted move constructor the requirements are not satisfied anymore so RVO must apply. That GCC does not do so in constant evaluation context seems like a bug to me as well. In constant evaluation context optional copy elision is never applied ([exp.const]/1), but that shouldn't affect the mandatory elision here. (I am not sure how [class.temporary]/3 is supposed to be handled in constant expressions though, since it is not mentioned.)

The Itanium C++ ABI specifies the type property non-trivial for the purpose of calls which mostly overlaps with the (inverse) of the condition in [class.temporary]/3. A return type without this property is passed according to the base C ABI, instead of having RVO applied as the Itanium C++ ABI specifies.

The underlying base C API is here the System V x86-64 psABI, which specifies (in 3.2.3 Returning of Values on page 24) that the return value for large structures (MEMORY class) is passed indirectly. Similarly to RVO the caller provides storage for the return value and passes a pointer to that space as additional argument to the function. However the ABI specification also includes the sentence

This storage must not overlap any data visible to the callee through
other names than this argument.

Which I guess is the reason that an additional copy needs to be performed. The ABI requires here that the explicitly provided pointer argument doesn't refer to the implicitly reserved storage for the return value, making RVO impossible.

Is there any way to execute the copy constructor when two user defined copy constructors present?

Answering your question... copy constructor no (2) can be executed

#include <iostream>

using namespace std;

class Class_Name
{
public:
int xx;

// Copy Constructor No. 1
Class_Name(Class_Name& objCopy)
{
cout << "Copy Constructor No. 1 called" << endl;
}

// Copy Constructor No. 2
Class_Name(const Class_Name& objCopy)
{
cout << "Copy Constructor No. 2 called" << endl;
}

Class_Name(int x) : xx(x) {} /* conversion constructor */
};

int main()
{
Class_Name obj1 = 1; // obj1 is not const, 1 is passed "by value", since it's primitive type
Class_Name objCopy1 = obj1; // obj1 is not const, other is not const, obj1 is passed by const reference

const Class_Name obj2 = 2; // obj2 is const
Class_Name objCopy2 = obj2;

return 0;
}

Output:

Copy Constructor No. 1 called

Copy Constructor No. 2 called

Copy Elision

Class_Name objCopy1 = Class_Name(obj1);

would be same as

Class_Name objCopy1 = obj1;

Taking the address of a returned temporary object

makeFoo and copy elision (which as you noted is guaranteed in this specific example since C++17) don't even matter.

activeFoo can never be dangling. If it was pointing to an object after its lifetime ended, then the destructor of that object would have reset activeFoo to nullptr, meaning it cannot be pointing to the object, a contradiction. That is assuming the destructor is called for every created object of type Foo. Technically this might not always be the case if you placement-new objects explicitly, although it should.

I would however not generally expect a static analyzer to figure out this logic. Without some details on the static analyzer or what exactly it complains about, it will be hard to say more.



Related Topics



Leave a reply



Submit