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
}