C++11 – Why Pass by Value Recommended if Const Reference Costs Single Copy?

c++c++11move-semantics

I am trying to understand move semantics, rvalue references, std::move, etc. I have been trying to figure out, by searching through various questions on this site, why passing a const std::string &name + _name(name) is less recommended than a std::string name + _name(std::move(name)) if a copy is needed.

If I understand correctly, the following requires a single copy (through the constructor) plus a move (from the temporary to the member):

Dog::Dog(std::string name) : _name(std::move(name)) {}

The alternative (and old-fashioned) way is to pass it by reference and copy it (from the reference to the member):

Dog::Dog(const std::string &name) : _name(name) {}

If the first method requires a copy and move both, and the second method only requires a single copy, how can the first method be preferred and, in some cases, faster?

Best Answer

Consider calling the various options with an lvalue and with an rvalue:

  1. Dog::Dog(const std::string &name) : _name(name) {}
    

    Whether called with an lvalue or rvalue, this requires exactly one copy, to initialize _name from name. Moving is not an option because name is const.

  2. Dog::Dog(std::string &&name) : _name(std::move(name)) {}
    

    This can only be called with an rvalue, and it will move.

  3.  Dog::Dog(std::string name) : _name(std::move(name)) {}
    

    When called with an lvalue, this will copy to pass the argument and then a move to populate the data member. When called with an rvalue, this will move to pass the argument, and then move to populate the data member. In the case of the rvalue, moving to pass the argument may be elided. Thus, calling this with an lvalue results in one copy and one move, and calling this with an rvalue results in one to two moves.

The optimal solution is to define both (1) and (2). Solution (3) can have an extra move relative to the optimum. But writing one function is shorter and more maintainable than writing two virtually identical functions, and moves are assumed to be cheap.

When calling with a value implicitly convertible to string like const char*, the implicit conversion takes place which involves a length computation and a copy of the string data. Then we fall into the rvalue cases. In this case, using a string_view provides yet another option:

  1. Dog::Dog(std::string_view name) : _name(name) {}
    

    When called with a string lvalue or rvalue, this results in one copy. When called with a const char*, one length computation takes place and one copy.