Using Std::Move() When Returning a Value from a Function to Avoid to Copy

Using std::move() when returning a value from a function to avoid to copy

No. Whenever a local variable in a return statement is eligible for copy elision, it binds to an rvalue re­fe­rence, and thus return t; is identical to return std::move(t); in your example with respect to which constructors are eligible.

Note however that return std::move(t); prevents the compiler from exercising copy elision, while return t; does not, and thus the latter is the preferred style. [Thanks to @Johannes for the cor­rect­ion.] If copy elision happens, the question of whether or not move construction is used becomes a moot point.

See 12.8(31, 32) in the standard.

Note also that if T has an accessible copy- but a deleted move-constructor, then return t; will not com­pile, because the move constructor must be considered first; you'd have to say something to the ef­fect of return static_cast<T&>(t); to make it work:

T f()
{
T t;
return t; // most likely elided entirely
return std::move(t); // uses T::T(T &&) if defined; error if deleted or inaccessible
return static_cast<T&>(t) // uses T::T(T const &)
}

When should std::move be used on a function return value?

In the case of return std::move(foo); the move is superfluous because of 12.8/32:

When the criteria for elision of a copy operation are met or would be
met save for the fact that the source object is a function parameter,
and the object to be copied is designated by an lvalue, overload
resolution to select the constructor for the copy is first performed as
if the object were designated by an rvalue.

return foo; is a case of NRVO, so copy elision is permitted. foo is an lvalue. So the constructor selected for the "copy" from foo to the return value of meh is required to be the move constructor if one exists.

Adding move does have a potential effect, though: it prevents the move being elided, because return std::move(foo); is not eligible for NRVO.

As far as I know, 12.8/32 lays out the only conditions under which a copy from an lvalue can be replaced by a move. The compiler is not permitted in general to detect that an lvalue is unused after the copy (using DFA, say), and make the change on its own initiative. I'm assuming here that there's an observable difference between the two -- if the observable behavior is the same then the "as-if" rule applies.

So, to answer the question in the title, use std::move on a return value when you want it to be moved and it would not get moved anyway. That is:

  • you want it to be moved, and
  • it is an lvalue, and
  • it is not eligible for copy elision, and
  • it is not the name of a by-value function parameter.

Considering that this is quite fiddly and moves are usually cheap, you might like to say that in non-template code you can simplify this a bit. Use std::move when:

  • you want it to be moved, and
  • it is an lvalue, and
  • you can't be bothered worrying about it.

By following the simplified rules you sacrifice some move elision. For types like std::vector that are cheap to move you'll probably never notice (and if you do notice you can optimize). For types like std::array that are expensive to move, or for templates where you have no idea whether moves are cheap or not, you're more likely to be bothered worrying about it.

Advantages of pass-by-value and std::move over pass-by-reference

  1. Did I understand correctly what is happening here?

Yes.


  1. Is there any upside of using std::move over passing by reference and just calling m_name{name}?

An easy to grasp function signature without any additional overloads. The signature immediately reveals that the argument will be copied - this saves callers from wondering whether a const std::string& reference might be stored as a data member, possibly becoming a dangling reference later on. And there is no need to overload on std::string&& name and const std::string& arguments to avoid unnecessary copies when rvalues are passed to the function. Passing an lvalue

std::string nameString("Alex");
Creature c(nameString);

to the function that takes its argument by value causes one copy and one move construction. Passing an rvalue to the same function

std::string nameString("Alex");
Creature c(std::move(nameString));

causes two move constructions. In contrast, when the function parameter is const std::string&, there will always be a copy, even when passing an rvalue argument. This is clearly an advantage as long as the argument type is cheap to move-construct (this is the case for std::string).

But there is a downside to consider: the reasoning doesn't work for functions that assign the function argument to another variable (instead of initializing it):

void setName(std::string name)
{
m_name = std::move(name);
}

will cause a deallocation of the resource that m_name refers to before it's reassigned. I recommend reading Item 41 in Effective Modern C++ and also this question.

return const value prevent move semantics

std::move is just a unconditional cast to rvalue. In your case the return value of std::move(foo()) was const std::string&&. And because move constructor does not take const argument, copy constructor was called instead.

struct C {
C() { std::cout << "constructor" << std::endl; }
C(const C& other) { std::cout << "copy constructor" << std::endl; }
C(C&& other) { std::cout << "move constructor" << std::endl; }
};

const C get() {
return C();
}

int main() {
C c(std::move(get()));
return 0;
}

Initialize vector without copying and using move semantics

If you use

std::vector<Ticker> table = std::vector<Ticker>{Ticker("MSFT"), Ticker("TSL")};

You will get

Parametrized constructor
Parametrized constructor
Copy constructor
Copy constructor
Destructor
Destructor
|MSFT|
|TSL|
Destructor
Destructor

Which has 4 constructor calls instead of the 6 you currently have. 2 of those calls are for Ticker("MSFT") and Ticker("TSL") and then the additional two copies are because initializer lists store the elements in them as const, so they have to be copied into the vector as you can't move from a const object.

To get the bare minimum of 2 constructor calls you'll need to use the emplace_back member function like

std::vector<Ticker> table;      // create empty vector
table.reserve(2); // allocate space for 2 Tickers but create nothing
table.emplace_back("MSFT"); // directly construct from "MSFT" in the reserved space
table.emplace_back("TSL"); // directly construct from "TSL" in the reserved space

which has the output of

Parametrized constructor
Parametrized constructor
|MSFT|
|TSL|
Destructor
Destructor

If you want a syntax like std::vector<Ticker> table = std::vector<Ticker>{Ticker("MSFT"), Ticker("TSL")};, but without the extra overhead, you could wrap the emplace_back solution in a factory function like

template <typename T, typename... Args> 
auto make_vector(Args&&... args)
{
std::vector<T> data;
data.reserve(sizeof...(Args));
(data.emplace_back(std::forward<Args>(args)), ...);
return data;
}

and then you would use it like

auto table = make_vector<Ticker>("MSFT", "TSL");


Related Topics



Leave a reply



Submit