One of the advantages of pass-by-value is that a function is free to modify a parameter without inadvertently changing the data at the calling location. Another advantage is that the function argument may be any expression - not just a variable.
Pass-by-reference is slightly more efficient than pass-by-value because it doesn't actually pass any data! Pass-by-reference makes the parameter refer to the same memory location as the argument. The compiler maps the parameter and the argument to the same memory location.
Pass-by-references is more efficient than pass-by-value, because it does not copy the arguments. The formal parameter is an alias for the argument. When the called function read or write the formal parameter, it is actually read or write the argument itself.
3.1: Pass Class Parameters by Reference What is surprising is that passing a complex object by reference is almost 40% faster than passing by value. Only ints and smaller objects should be passed by value, because it's cheaper to copy them than to take the dereferencing hit within the function.
/* (0) */
Creature(const std::string &name) : m_name{name} { }
A passed lvalue binds to name
, then is copied into m_name
.
A passed rvalue binds to name
, then is copied into m_name
.
/* (1) */
Creature(std::string name) : m_name{std::move(name)} { }
A passed lvalue is copied into name
, then is moved into m_name
.
A passed rvalue is moved into name
, then is moved into m_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 into m_name
.
A passed rvalue binds to rname
, then is moved into m_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.
How you pass is not the only variable here, what you pass makes the big difference between the two.
In C++, we have all kinds of value categories and this "idiom" exists for cases where you pass in an rvalue (such as "Alex-string-literal-that-constructs-temporary-std::string"
or std::move(nameString)
), which results in 0 copies of std::string
being made (the type does not even have to be copy-constructible for rvalue arguments), and only uses std::string
's move constructor.
Somewhat related Q&A.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With