C++ from Basics to Mastery — Part 19
Preserve value categories with perfect forwarding
The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.
Even after learning move semantics, a generic wrapper can accidentally turn every argument into an lvalue. Perfect forwarding preserves whether the caller supplied an lvalue or an rvalue when the wrapper calls the next function.
A named expression is an lvalue
void Store(const Texture& texture); // read
void Store(Texture&& texture); // may take ownership
Texture loaded = LoadTexture();
Store(loaded); // lvalue overload
Store(std::move(loaded)); // rvalue overload
Store(LoadTexture()); // rvalue overload
An lvalue identifies a persistent object. An rvalue may denote a temporary or an object explicitly offered for moving. Even a variable whose type is Texture&& is an lvalue expression when used by name.
Forwarding references and reference collapsing
template <typename T>
void Register(T&& value)
{
Store(value);
}
When T is deduced, this T&& is a forwarding reference. Passing an lvalue deduces an lvalue-reference form of T; passing an rvalue retains an rvalue reference after reference collapsing.
Inside the function, however, value has a name, so Store(value) always chooses the lvalue overload.
Restore the original category with std::forward
#include <utility>
template <typename T>
void Register(T&& value)
{
Store(std::forward<T>(value));
}
std::forward<T> forwards an original lvalue as an lvalue and an original rvalue as an rvalue. Unlike std::move, it does not unconditionally cast to an rvalue.
template <typename T, typename... Args>
std::unique_ptr<T> MakeOwned(Args&&... args)
{
return std::make_unique<T>(std::forward<Args>(args)...);
}
Factories use this pattern to relay constructor arguments without inventing copies.
Not every API needs forwarding references
For a setter that stores a string, taking by value and moving once is often simpler.
void Player::SetName(std::string name)
{
Name = std::move(name);
}
Forwarding references accept a very broad overload set and can produce difficult diagnostics. Use them in genuinely transparent generic adapters. At application boundaries, prefer a concrete type and an explicit ownership contract.