본문으로 건너뛰기

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 → param becomes an lvalue reference.
  • rvalue argument → param becomes 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
  • auto type deduction
  • typedef
  • 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
  • 0 and NULL
  • Declaration-only integral static const members
  • Overloaded function names
  • Function template names
  • Bitfields