Home > Software engineering >  When to rely on RVO vs move semantics in C ?
When to rely on RVO vs move semantics in C ?

Time:01-01

Say I have some expensive class X, and take this code:

X functor() {
    X x;
    //do stuff
    return x;
}

int main() {
    std::vector<X> vec;
    vec.push_back(functor());
    vec.push_back(std::move(functor()));

    return 0;
}

Which push_back is more efficient? In the first case, won't the NRVO be activated and prevent the copy as the move does? Should I be relying on the NRVO instead of doing manual moving, since the NRVO is basically automatic moving?

CodePudding user response:

Neither piece of code is more efficient; they do the exact same thing. And neither push_back expression involves elision of any kind. The only actual elision happened back in the return statement, which the push_back expressions don't interact with.

In both push_back expressions, the prvalue returned by the function will manifest a temporary. In the latter case, the temporary will be cast into an xvalue, but that doesn't represent any actual runtime changes. Both versions will call the same push_back overload: the one which takes an rvalue reference. And therefore in both cases the return value will be moved from.

CodePudding user response:

NRVO implies that automatic storage described by x is identical to storage referenced by rvalue result of functor() call. std::move() in this case is late for the party and gets no credit.

CodePudding user response:

@Nichol Bolas' answer is correct, as far as it goes.

It seems to me that you don't entirely understand what std::move does, or when it's intended to be used.

std::move is (at least primarily) used when you have a parameter or rvalue reference type, something like this: int foo(int &&param);. The problem is that even though param has to be initialized from an rvalue, and refers to an rvalue, param itself has a name (i.e., param) so it is not itself an rvalue. Because of that, the compiler can't/won't automatically recognize that param can be used as the source of a move operation. So, if you want to do a move out of param, you need to use std::move to tell the compiler that this is an rvalue, so the compiler can move from it instead of copying from it.

In your code: vec.push_back(std::move(functor()));, you're applying the std::move to the temporary value that holds the return from the function. Since this is a temporary rather than a named variable, the compiler can/will automatically recognize that it can be used as the source of a move, so std::move has no hope of accomplishing anything.

Although it doesn't really apply in this specific case, the other point to keep in mind for situations somewhat like this is using emplace_back instead of push_back. This can allow your code to avoid building and then copying/moving an object at all. Instead, it can create references to the parameters that you pass to the ctor to create the object, so that inside of emplace_back itself, you create an object (once) in the spot it's going to occupy in the target vector. For example:

struct foo { 
    int i;
    double d;

    foo(int i, double d) : i(i), d(d) {}
};

std::vector<foo> f;

int a = 123;
double b = 456.789;

f.push_back(foo(a, b));
f.emplace_back(a, b);

In this case, the push_back creates a temporary foo object, initialized from a and b . Since that's a temporary, the compiler recognizes that it can do a move from there into the spot it's going to occupy in the vector.

But the emplace_back avoids creating the temporary at all. Instead, it just passes references to a and b, and inside of emplace_back, when it's ready to create the object in the vector, it creates it, initializing it directly from a and b.

foo directly contains data, rather than containing a pointer to the data. That mean in this case, doing a move is likely to gain little or nothing compared to doing a copy, so even though push_back can accept an rvalue and do a move out of it into the target, it wont' do much good in this case--it'll end up copying the data anyway. But with emplace_back, we'll avoid that completely, and just initialize the object directly from the source data (a and b), so we'll copy them once--but only once.

  • Related