How to efficiently return an object when copy/move elision is disabled?

Assuming copy elision is disabled when compiling, is the following a valid thing to do to achieve the riddance of unnecessary copying (simulating copy elision)?

Container getContainer() {
    Container c;

    return c;
}

int main() {
    Container&& contianer = getContainer();
}

  • 5

    You talk about NRVO, or in main? (With C++17, some “copy elision” are mandatory).

    – 

  • 1

    If copy elision is disabled then you can always do it manually, e.g. return by passing ref arg. That’s how compiler would do it anyway.

    – 




  • 4

    Container&& contianer = getContainer(); is pointless btw. Container contianer = getContainer(); behaves exactly the same except that the first is harder to understand. Since C++17 both do not make a copy (regardless of whether or not copy elision is enabled) and before C++17 they both make a copy if copy elision is not done.

    – 

  • 2

    when copy elision is disabled you make a copy, what else do aim for?

    – 

  • 2

    @chakmeshma why don’t you want to rely on a compiler? As long as you follow some basic rules copy elision is mandatory.

    – 

Since C++17, some forms of copy elision ceased to be copy elision because the language simply no longer specifies the creation of a temporary anymore. Thus, there is nothing for the compiler to elide in the first place. For example, you can write this:

Container container = getContainer();

and there’s no temporary object created for the return value of getContainer. Instead, the program directly constructs the result into container. Therefore, you do not need to write Container&& container = getContainer().

On the other hand, NRVO is not guaranteed. When we look at this function definition:

Container getContainer() {
    Container c;

    return c;
}

you are creating a local variable named c and requesting the move-initialization of the return object from c. If the compiler performs NRVO, then the variable c is elided. However, if you want a guarantee that the local Container object is elided (and thus no move is required) then the way to do that is to not declare a local Container object in the first place.

In simple cases you can just create the return value at the last minute:

Container getContainer() {
    // ...
    return Container();
}

There is no temporary object created by the above code (since C++17).

However, if you need to construct the return value, do something with it, and then later return, you may need to declare your function to take an out-parameter of type Container&, which the function will mutate into the value that it wants to return. In some cases there might be no way for the caller to construct a Container object to pass into the function. In that case, the function must take a pointer argument, into which the value will be constructed using placement new. In that case the caller must take care to call the destructor manually at the end of the scope.

Copy elision is guaranteed since C++17, unless you use the flag to disable, such as -fno-elide-constructors in gcc, for example.

If you disable copy elision, and you still want a way to efficiently return an object, which I assume by efficiently you mean low runtime overhead, then temporary object lifetime extension is the way to go. You can use const lvalue reference or forwarding reference to extend the lifetime of the temporary.

Is move elision a thing? Keep in mind that if you do std::move for the returned value, it will mess up the RVO and NRVO. Current standard wordings currently only deal with object as a returned value for RVO/NRVO, std::move will return a rvalue reference, which is not an object in C++.

I am not qualified enough to discuss the nuances of the standard regarding NRVO but we can try and test this similarly to the below snippet (using GCC 13.2 and C++11 mode plus the -fno-elide-constructors option. live):

#include <vector>
#include <cstdio>


std::vector<int> create( const int in )
{
    std::vector<int> foo ( 5, in );
    return foo;
}

int main( int argc, char* argv[] )
{
    std::vector<int>&& result { create( argc ) }; // remove `&&` and it will result
                                                  // in slightly more assembly code

    for ( auto&& val : result )
        std::printf( "%d\n", val );
}

It seems that adding && to the type of result removes the need for a few extra mov and movdqa instructions. With that said, you’ll probably need to measure the performance differences. The generated machine code might be misleading in some cases.

Clang 17.0.1 on the other hand generates identical code for both cases (with or without &&).

Leave a Comment