Following this interesting StackOverflow question about RVO failure, I’ve experimented a little to see in which cases copies are elided by the compiler. The question is about RVO failing in case when NRVO is performed properly; it is hard to explain why this happens without knowing the internals of the MSVC compiler, but here are a few observations on when this happens.

The same behavior can be reproduced with simple classes instead of the rather complex std::vector:

#include <iostream>

struct C
{
    C(const C& in) { std::cout << "C(&) <-- copy\n";}
    C(C&& in) {}

    C() {}
    ~C() {}
};

struct D
{
    C c;

    D(C _c) : c(std::move(_c)) { std::cout << "D(C)\n";}
};

D test_RVO()
{
    C c;
    return D(std::move(c)); 
}

D test_NRVO()
{
    C c;
    D d(std::move(c));
    return d;
}

int main()
{
    std::cout << "RVO\n";
    test_RVO(); // or auto val = test_RVO();
    
    std::cout << "\nNRVO\n";
    test_NRVO();
}

When built and run with MSVC 2013 with optimizations enabled, it produces the following output:

RVO
D(C)
C(&) <-- copy

NRVO
D(C)

try online (as long as their compiler is not upgraded to 2014+)

In test_NRVO(), D is not copied due to the NRVO optimization, but the equivalent RVO optimization fails for test_RVO(). Note that in debug mode (/Od) the results might be different as some optimizations are disabled and not all copies are elided. This has been changed in MSVC 2014/15 where even in debug mode all copies seem to be elided (this RVO behavior is fixed too). Gcc and clang elide all copies including the non-optimized builds (there is -fno-elide-constructors option though).

Taking a closer look

First, note that the same issue occurs when instead of std::move the object is passed directly to the D’s constructor:

D test_RVO()
{
    return D(C());
}

Outputs

D(C)
C(&) <-- copy

Next, adding traces to move constructors

//...
    C(C&& in) { std::cout << "C(&&)\n"; }
//...
    D(D&& in) : c(in.c) { std::cout << "D(&&)\n"; }
//...

produces the following ouput for D(C())

RVO
C(&&)
D(C)
C(&) <-- copy
D(&&)

C(&&) is printed from the D(C _c) constructor as per c(std::move(_c)), which is then followed by the constructor’s D(C) trace. Then, however, the D instance is being moved when returning from test_RVO(). The move in its turn calls c(in.c) and this invokes C’s copy constructor. This C copy could be turned into a move by doing c(std::move(in.c)) in the D(D&&) constructor, but this is somewhat irrelevant: the question is why the whole D’s move is not elided by RVO in the first place. NRVO does elide it (there is no D(&&)):

NRVO
C(&&)
C(&&)
D(C)

Constructor parameter type matters

It turns out that the reason of the RVO failure is linked to the properties of the type that is being passed in the constructor D(C _c). For example, using a different type in the constructor magically “fixes” RVO:

struct X
{
};

struct D
{
    //...
    D(X _x) : c() { std::cout << "D(X)\n"; }
};

D test_RVO()
{
    return D(X());
}

Outputs

RVO
D(X)

There is no D(&&) meaning no move is performed, and RVO works. How is D(X _x) different from D(C _c)? Let’s add a few things to X:

struct X
{
    X() {}
    ~X() {}

    X(X const&) {}
};

And now RVO suddenly fails:

RVO
D(X)
C(&) <-- copy
D(&&)

Wait. What?

It turns out that RVO is performed correctly with the D(X) constructor, but it starts failing when (-a- and (-b- or -c-)) are uncommented below:

struct X
{
    X() {}
    //~X() {}           -a-
    
    //X(X const&) {}    -b-
    //X(X&&) {}         -c-
};

In other words, a constructor of type D with signature D(T) leads to RVO failure when used as above, when the type T has a non-default destructor and a non-default copy (or move) constructor. Certainly, this applies to that SO example where T is std::vector implementing all of those methods.

A couple of final notes:

  • This is not specific to values returned from functions. For example, the same applies to expressions like D d = D(T());, where MSVC <2014 does not elide the copy/move (under the same conditions).

  • There is no RVO failure when the constructor is replaced with a static method returning by value:

struct D
{
    //...
    static D D::construct(X _x)
    {
        return D();
    }
};

D test_RVO()
{
    return D::construct(X()); // <-- RVO is performed correctly

    // return D(X());         // <-- but not this way
}


blog comments powered by Disqus