Rvalue References, Move Semantics, and Perfect Forwarding
5.1 Understand std::move and std::forward
1) Move Semantics
Move Semantics:
A mechanism that transfers resources from one object to another instead of copying them.
Moving can avoid expensive resource duplication.
2) std::move
std::move:
A function that unconditionally casts its argument to an rvalue.
std::string s1 = "hello";
std::string s2 = std::move(s1);
std::move itself does not move anything.
It only makes the argument eligible to be handled by move operations.
3) std::forward
std::forward:
A conditional cast that preserves the original value category of an argument.
template<typename T>
void f(T&& param)
{
g(std::forward<T>(param));
}
It casts to an rvalue only when the original argument was an rvalue.
4) const and Moving
Moving from a const object usually results in copying.
const std::string s = "hello";
std::string x = std::move(s);
std::move(s) has type const std::string&&, but most move constructors require a non-const rvalue reference.
string(string&& rhs);
Therefore, objects intended to be moved from should generally not be declared const.
5.2 Distinguish Universal References from Rvalue References
1) Rvalue Reference
Rvalue Reference:
A reference declared with && that binds to rvalues.
Widget&& w = Widget{};
An && declaration is an rvalue reference when type deduction is not involved.
2) Universal Reference
Universal Reference:
A reference of the form T&& involving type deduction that can bind to both lvalues and rvalues.
template<typename T>
void f(T&& param);
- lvalue argument →
parambecomes an lvalue reference. - rvalue argument →
parambecomes an rvalue reference.
3) Universal Reference Conditions
A reference is universal when:
- Type deduction occurs.
- The declaration has exactly the form
T&&.
template<typename T>
void f(T&& x); // universal reference
template<typename T>
void f(const T&& x); // rvalue reference
const T&& is not a universal reference.
4) auto&&
auto&& also forms a universal reference because type deduction occurs.
auto&& x = value;
It can bind to either an lvalue or an rvalue.
5.3 Use std::move on Rvalue References and std::forward on Universal References
1) Rvalue Reference Parameters
Use std::move when passing an rvalue reference parameter onward.
class Widget {
public:
Widget(Widget&& rhs)
: name(std::move(rhs.name))
{}
private:
std::string name;
};
An rvalue reference parameter represents an object known to be eligible for moving.
2) Universal Reference Parameters
Use std::forward with universal references.
template<typename T>
void setName(T&& name)
{
value = std::forward<T>(name);
}
This preserves whether the original argument was an lvalue or an rvalue.
3) Named References Are Lvalues
A named variable is an lvalue expression even if its declared type is an rvalue reference.
void f(Widget&& w)
{
g(w); // w is an lvalue expression
g(std::move(w)); // treated as rvalue
}
This is why std::move or std::forward is necessary inside forwarding functions.
4) Return Value Optimization
Do not apply std::move to local objects being returned by value when copy elision can occur.
Widget makeWidget()
{
Widget w;
return w;
}
Prefer this over:
return std::move(w);
Unnecessary std::move can prevent return value optimization.
5.4 Avoid Overloading on Universal References
1) Universal Reference Overload
Universal-reference overloads can match more argument types than expected.
void logAndAdd(int idx);
template<typename T>
void logAndAdd(T&& name);
The template may become a better match even when another overload appears more appropriate.
2) Overload Resolution
A universal reference can preserve the exact argument type.
This can make it a better overload match than functions requiring conversions.
short x = 10;
logAndAdd(x);
The universal-reference overload may be selected instead of the int overload.
3) Perfect-Forwarding Constructor
Perfect-Forwarding Constructor:
A constructor taking a universal reference and forwarding its argument.
class Person {
public:
template<typename T>
explicit Person(T&& name)
: name(std::forward<T>(name))
{}
private:
std::string name;
};
Such constructors can interfere with copy and move constructors.
4) Copy Constructor Hijacking
For a non-const lvalue, a forwarding constructor may be a better match than the copy constructor.
Person p1("Nancy");
Person p2(p1);
The template may deduce T as Person&, causing the forwarding constructor to be selected.
5) General Rule
Avoid overloading functions on universal references unless their accepted types are deliberately constrained.
5.5 Alternatives to Overloading on Universal References
1) Abandon Overloading
Use different function names instead of overloads.
logAndAddName(name);
logAndAddNameIdx(index);
This completely avoids overload-resolution conflicts.
2) Pass by const T&
Use a const lvalue reference when maximum forwarding efficiency is unnecessary.
void logAndAdd(const std::string& name);
This is simple and predictable but may require additional copies.
3) Pass by Value
Pass by value when the function will copy the argument anyway.
void setName(std::string name)
{
value = std::move(name);
}
Rvalues can be moved into the parameter, while lvalues are copied.
4) Tag Dispatch
Tag Dispatch:
A technique that forwards to different implementations based on compile-time type information.
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<
std::remove_reference_t<T>
>{}
);
}
Different overloads can then handle std::true_type and std::false_type.
5) std::enable_if
std::enable_if:
A template mechanism that conditionally removes a function from overload resolution.
It can constrain which types a universal-reference function accepts.
template<
typename T,
typename = std::enable_if_t<
!std::is_integral<
std::remove_reference_t<T>
>::value
>
>
void f(T&& value);
This allows perfect forwarding while preventing unwanted matches.
5.6 Understand Reference Collapsing
1) Reference Collapsing
Reference Collapsing:
The rule that determines the final reference type when a reference is applied to another reference type.
C++ collapses reference combinations according to fixed rules.
2) Collapsing Rules
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
The result is an rvalue reference only when both references are rvalue references.
Otherwise, the result is an lvalue reference.
3) Universal Reference Deduction
When an lvalue is passed to a universal reference:
template<typename T>
void f(T&&);
Widget w;
f(w);
T is deduced as:
Widget&
Therefore:
T&&
→ Widget& &&
→ Widget&
Reference collapsing explains why universal references can bind to lvalues.
4) Reference Collapsing Contexts
Reference collapsing occurs in contexts including:
- Template type deduction
autotype deductiontypedef- Alias declarations
decltype
5.7 Assume Move Operations Are Not Present, Not Cheap, and Not Used
1) Move Is Not Always Available
Not every type supports move operations.
Legacy C++98-style classes may only provide copying.
class Widget {
public:
Widget(const Widget&);
};
Attempting to move such an object may fall back to copying.
2) Move Is Not Always Cheap
Move operations can still require significant work.
For some types, moving may have complexity similar to copying.
Do not assume that every move operation is constant-time.
3) Move May Not Be Used
A move operation is normally selected only when the source can bind to the move operation's rvalue-reference parameter.
const objects commonly prevent moving.
const Widget w;
Widget w2 = std::move(w);
This may invoke the copy constructor.
4) Known Modern Types
When working with known types that explicitly support efficient move operations, move semantics can provide significant performance benefits.
In generic code, however, do not assume that moving is available or cheap.
5.8 Familiarize Yourself with Perfect Forwarding Failure Cases
1) Perfect Forwarding
Perfect Forwarding:
Passing an argument to another function while preserving its original type and value category.
template<typename T>
void fwd(T&& param)
{
f(std::forward<T>(param));
}
Perfect forwarding works when template type deduction can correctly determine the argument type.
2) Braced Initializers
Braced initializers cannot normally be directly perfect-forwarded.
f({1, 2, 3}); // may work
fwd({1, 2, 3}); // deduction fails
A workaround is to first create an std::initializer_list.
auto values = {1, 2, 3};
fwd(values);
3) Null Pointer Constants
0 and NULL are deduced as integral types, not pointer types.
fwd(0);
fwd(NULL);
Use nullptr when forwarding null pointers.
fwd(nullptr);
4) Declaration-Only Integral static const Members
A static const integral member may be usable as a compile-time value without a definition.
However, forwarding it by reference can require storage and therefore a definition.
class Widget {
public:
static const std::size_t MinVals = 28;
};
If perfect forwarding causes an ODR-use, an out-of-class definition may be necessary in pre-C++17 code.
5) Overloaded Function Names
A function name referring to multiple overloads does not identify one specific type.
void process(int);
void process(int, int);
fwd(process); // deduction fails
The intended function type must be made explicit.
using ProcessFunc = void(*)(int);
ProcessFunc pf = process;
fwd(pf);
6) Function Templates
Function template names can have the same problem because they represent multiple possible functions.
The desired specialization must be explicitly selected before forwarding.
7) Bitfields
Bitfields cannot bind to ordinary non-const references.
Therefore, they cannot be directly perfect-forwarded.
struct IPv4Header {
std::uint32_t version : 4;
};
IPv4Header h;
fwd(h.version); // problem
Copy the bitfield value to a normal object first.
auto version = h.version;
fwd(version);
8) General Failure Rule
Perfect forwarding can fail when template type deduction cannot determine the intended argument type or when the deduced type cannot be represented as the required reference.
Important cases are:
- Braced initializers
0andNULL- Declaration-only integral
static constmembers - Overloaded function names
- Function template names
- Bitfields