Advantages of pass-by-value and std::move over pass-by-reference
/* (0) */
Creature(const std::string &name) : m_name{name} { }
A passed lvalue binds to
name
, then is copied intom_name
.A passed rvalue binds to
name
, then is copied intom_name
.
/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
A passed lvalue is copied into
name
, then is moved intom_name
.A passed rvalue is moved into
name
, then is moved intom_name
.
/* (2) */
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
A passed lvalue binds to
name
, then is copied intom_name
.A passed rvalue binds to
rname
, then is moved intom_name
.
As move operations are usually faster than copies, (1) is better than (0) if you pass a lot of temporaries. (2) is optimal in terms of copies/moves, but requires code repetition.
The code repetition can be avoided with perfect forwarding:
/* (3) */
template <typename T,
std::enable_if_t<
std::is_convertible_v<std::remove_cvref_t<T>, std::string>,
int> = 0
>
Creature(T&& name) : m_name{std::forward<T>(name)} { }
You might optionally want to constrain T
in order to restrict the domain of types that this constructor can be instantiated with (as shown above). C++20 aims to simplify this with Concepts.
In C++17, prvalues are affected by guaranteed copy elision, which - when applicable - will reduce the number of copies/moves when passing arguments to functions.
- Did I understand correctly what is happening here?
Yes.
- Is there any upside of using
std::move
over passing by reference and just callingm_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.