Consider calling the various options with an lvalue and with an rvalue:
-
Dog::Dog(const std::string &name) : _name(name) {}Whether called with an lvalue or rvalue, this requires exactly one copy, to initialize
_namefromname. Moving is not an option becausenameisconst. -
Dog::Dog(std::string &&name) : _name(std::move(name)) {}This can only be called with an rvalue, and it will move.
-
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:
-
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.