Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving local variable into return value of different type

When returning values from a function in C++, we have copy elision and (Named) Return Value Optimization helping us create more efficient code. In short, the following code:

std::vector<int> make_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v;
}

results in a silent move or direct construction into the destination of the return value, instead of a copy. The rules around this also mean that explicitly moving the returned object when returning actually prevents these optimizations.

std::vector<int> make_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // BAD
}

This version prevents RVO, as explained in Scott Meyers' Effective Modern C++, Item 25.


My question is what happens when the return type is different, but can be move-constructed from one or more local variables? Consider the following functions that each return an optional vector:

std::optional<std::vector<int>> make_opt_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v; // no move
}

std::optional<std::vector<int>> make_opt_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // move
}

Which of these is correct? The line return std::move(v) looks like a red flag to me at first, but I also suspect it's the correct thing to do here. The same goes for the following two functions returning a pair of vectors:

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_1(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {v1, v2}; // no move
}

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_2(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {std::move(v1), std::move(v2)}; // move
}

In this case too, despite looking weird at first glance, I think moving into the return value is the better thing to do.

Am I correct that it's better to move into the return value when the types differ, but the return value can be move constructed from the local variable(s) being moved from? Have I misunderstood NRVO, or is there some other optimization that is well ahead of me here?

like image 465
alter_igel Avatar asked Jun 06 '19 19:06

alter_igel


1 Answers

Am I correct that it's better to move into the return value when the types differ, but the return value can be move constructed from the local variable(s) being moved from? Have I misunderstood NRVO, or is there some other optimization that is well ahead of me here?

You did miss out on one thing. Even if the types differ, there will be an implicit move done automatically.

[class.copy.elision] (emphasis mine)

3 In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or

  • if the operand of a throw-expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one),

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided.  — end note ]

That is not contingent on the types matching, and is a fallback behavior in case full (N)RVO doesn't happen. You therefore gain nothing by moving explicitly in make_opt_vec_2.

Given that std::move is either a pessimization or entirely superfluous, I'd argue it's best not to do it when simply returning a function local object.

The only case where you'd want to write the move explicitly, is when the expression you return is more complex. In that case, you are indeed on your own, and not moving is a potential pessimization. So in make_vec_pair_2, moving into the pair is the correct thing to do.

The rule of thumb here is to not move just an id-expression that is a function local object. Otherwise, move away.

like image 67
StoryTeller - Unslander Monica Avatar answered Oct 24 '22 05:10

StoryTeller - Unslander Monica