Understanding std::forward
/ 3 min read
Table of Contents
std::move and std::forward are closely related but they solve a different problem: perfect forwarding.
What std::forward Actually Does
Like std::move, std::forward doesn’t actually forward anything. It’s a conditional cast that preserves the “value category” of its argument. If you pass an rvalue, it stays an rvalue. If you pass an lvalue, it stays an lvalue.
template<typename T>void wrapper(T&& arg) { target_function(std::forward<T>(arg)); // Preserves value category}Without std::forward, everything becomes an lvalue inside the template function, even if you originally passed an rvalue.
The Problem It Solves
Before perfect forwarding, writing generic wrapper functions was annoying. You had to provide overloads for every combination of lvalue and rvalue references:
// lots of overloadsvoid wrapper(const std::string& s) { target_function(s); }void wrapper(std::string&& s) { target_function(std::move(s)); }With perfect forwarding, one template handles all cases:
// The new way - one template for everythingtemplate<typename T>void wrapper(T&& arg) { target_function(std::forward<T>(arg));}When I Actually Use std::forward
Most of the time, I use std::forward in these situatoins:
- Factory functions: When creating objects and passing arguments:
template<typename T, typename... Args>std::unique_ptr<T> make_object(Args&&... args) { return std::make_unique<T>(std::forward<Args>(args)...);}
auto obj1 = make_object<MyClass>(some_string); // lvalueauto obj2 = make_object<MyClass>(std::move(temp)); // rvalue- Wrapper functions: When I need to forward arguments to another function:
template<typename F, typename... Args>auto time_function(F&& func, Args&&... args) { auto start = std::chrono::steady_clock::now(); auto result = func(std::forward<Args>(args)...); auto end = std::chrono::steady_clock::now(); return result;}- Constructors that forward to other constructors:
class MyClass { std::string name_; std::vector<int> data_;
public: template<typename... Args> MyClass(std::string name, Args&&... args) : name_(std::move(name)), data_(std::forward<Args>(args)...) {}};The Key Difference
The main difference between std::move and std::forward:
std::movealways casts to rvalue referencestd::forwardconditionally casts based on the template parameter
std::string text = "hello";
std::move(text); // Always rvalue referencestd::forward<T>(text); // Depends on what T isUniversal References
std::forward only makes sense with universal references (the T&& in templates). Regular rvalue references don’t need forwarding:
// Universal reference - use std::forwardtemplate<typename T>void func(T&& arg) { other_func(std::forward<T>(arg)); }
// Regular rvalue reference - use std::movevoid func(std::string&& arg) { other_func(std::move(arg)); }The Rule I Follow
I use std::forward when I have a template function that gets universal reference and needs to pass them to another function. It’s all about preserving the original intent - if someone passed an rvalue, they probably want move semantics.
The pattern is almost always the same: std::forward<T>(arg) where T is the template parameter and arg is the universal reference parameter.
Perfect forwarding is what makes functions like std::make_unique and std::make_shared work efficiently - they can construct objects with any combination of arguments without unnecessary copies.