Copy/move elision requires explicit definition of copy/move constructors
Asked Answered
D

1

11

Consider the following program:

#include <iostream>
#include <utility>

class T {
public:
    T() { printf("address at construction:    %zx\n", (uintptr_t)this); }
    // T(const T&) { printf("copy-constructed\n"); } // helps
    // T(T&&) { printf("move-constructed\n"); }      // helps
    // T(const T&) = default;                        // does not help
    // T(T&&) = default;                             // does not help
};

T f() { return T(); }

int main() {
    T x = f();
    printf("address after construction: %zx\n", (uintptr_t)&x);
    return 0;
}

Compiling with g++ -std=c++17 test.cpp gives the following output (same with clang++):

address at construction:    7ffcc7626857
address after construction: 7ffcc7626887

Based on the C++ reference I would expect the program to output two equal addresses because the copy/move should be guaranteed to be elided (at least in C++17).

If I explicitly define either the copy or the move constructor or both (see commented out lines in the example), the program gives the expected output (even with C++11):

address at construction:    7ffff4be4547
address after construction: 7ffff4be4547

Simply setting the copy/move constructors to default does not help.

The reference explicitly states

[The copy/move constructors] need not be present or accessible

So what am I missing here?

Dubose answered 7/4, 2018 at 7:39 Comment(1)
Related: #48879726Curule
C
9

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 ]

Curule answered 7/4, 2018 at 8:7 Comment(4)
Interesting, it seems to me though that the standard formulates this exception too broadly. The motivation for my question was that my T is rather large and my stack very small, so I care strongly about not duplicating the object on the stack. If this pattern occurs multiple times, adding an explicit copy constructor to every class feels like a hack though, is there anything I can do in f() to avoid this?Dubose
@AlGebra - But your T here isn't large, it is extremely small. A very small object that is constructed in a register and returned by value might not need any stack space at all. And so it might be more efficient than involving the RVO machinery, which at least needs some indirection.Prosecution
@AlGebra: Stop second-guessing your compiler. If your actual T really is "rather large", then the compiler won't perform this optimization. So let your compiler do its job, so that you can get back to doing your job: writing a functioning program.Knick
@BoPersson @NicolBolas Thanks for pointing this out. When adding const int data[5] = {0}; to T the compiler does indeed elide the move. However in my real use case the objects are in fact duplicated on the stack which is why I investigated this in the first place. I must have missed some other relevant detail while isolating the issue.Dubose

© 2022 - 2024 — McMap. All rights reserved.