How to enforce copy elision in C++20?

269 Views Asked by At

C++17 promised to introduce Copy Elision as a requirement, so I've upgraded from C++14 all the way to C++20. Just for that. (RVO as an optional behavior-altering optimization... makes me genuinely motion-sick when running a program in my head.) I'm very new to this version of C++, but I want to prescribe the behavior of returning an object completely; whether it calls it's copy method and a temp's destructor, or does not.

Object f() {
    Object x;
    x.value = 10;
    x.str = "example function";
    return x;
}

Is there anything special I must do (or change) in this f() function to ensure the returned object always elides calling it's copy or move constructor and x's destructor? Also, is there any way to ask the compiler to abort compilation if it can't do so? I don't want to accidentally give the compiler the ability to choose, if I can help it.

2

There are 2 best solutions below

1
Nelfeal On BEST ANSWER

Check out cppreference's page on copy elision. Since C++17, there is a guaranteed form and a non-mandatory form. Technically, guaranteed copy elision is not even considered copy elision anymore, it is simply a change in the specification of prvalues.

"Guaranteed copy elision" happens when initializing an object (including in a return statement) with a prvalue of the same class type (ignoring cv-qualification). By definition, a prvalue has no name, and so, before C++17 and in the case of a return statement, this was an non-mandatory optimization called unnamed return value optimization (URVO). Since C++17, it is replaced by a fundamental change in the language:

a prvalue is not materialized until needed, and then it is constructed directly into the storage of its final destination.

In your code, x is named, so it is not a prvalue, and as such its copy (or move) is not guaranteed to be elided. Nevertheless, I believe most modern compilers will perform copy elision in this case, unless you specifically ask them not to (for example with -fno-elide-constructors for GCC and Clang). It is called named return value optimization (NRVO), and it is one of only two optimizations that may change the observable side-effects. Note that NRVO is forbidden in the context of a constant expression.

If you want a guarantee that no copy or move is performed, then you need to handle a prvalue instead:

Object f() {
    int value = 10;
    std::string str = "example function";
    return Object(value, str); // this is a prvalue being returned
}

// simpler
Object f() {
    return Object(10, "example function");
}

// even simpler if Object's constructor is implicit
Object f() {
    return {10, "example function"};
}
0
Jan Schultke On

Optional NRVO

The example that you've shown is an example of Named Return Value Optimization (NRVO). In such a case, the compiler is allowed to, but not required to perform copy elision. This means that the copy/move constructor of Object may be called, but it may also be omitted.

[...] called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile object with automatic storage duration [...] with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the object directly into the function call's return object.

- [class.copy.elision] p1, p1.1

C++17 and C++20 have the same rule in this regard.

Mandatory RVO

What actually changed in C++17 is that plain Return Value Optimization (RVO) became mandatory. This kicks in whenever you return a prvalue, such as:

Object f() {
    return {10, "example function"};
}
// or in C++20, with designated initializers
Object f() {
    return {.value = 10, .str = "example function"};
}

In these two cases, a C++17 compiler is required to elide the copy:

[...]; the return statement initializes the returned reference or prvalue result object of the (explicit or implicit) function call by copy-initialization from the operand.

- [stmt.return] p2

This means that return { ... }; works as if you wrote Object x = { ... }; at the call site; and for copy-initialization:

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object.

- [dcl.init.general] p16.6.1

This rule means that the { ... } initializes the Object at the call site directly. The two rules combined imply return value optimization.