Perfect Forwarding
Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 84 of 104.
When writing template functions that accept arguments and pass them to other functions, a problem arises: how do you preserve whether the original argument was an lvalue or rvalue? Perfect forwarding solves this by passing arguments exactly as they were received, maintaining their value category.
The key ingredients are forwarding references (written as T&& in a template context) and std::forward. A forwarding reference can bind to both lvalues and rvalues, and std::forward conditionally casts the argument back to its original type:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // Calls process(int&) - lvalue preserved
wrapper(20); // Calls process(int&&) - rvalue preserved
}Without std::forward, named parameters are always lvalues inside the function, even if an rvalue was passed. This would prevent move semantics from working correctly when forwarding to constructors or other functions.
Perfect forwarding is essential for writing factory functions, wrapper classes, and any generic code that needs to construct objects or invoke functions while preserving the caller's intent regarding moves versus copies.
Challenge
EasyLet's build a factory function that demonstrates perfect forwarding by constructing objects while preserving the value category of the arguments passed to it. You'll create a generic make_object function that forwards arguments to a class constructor, ensuring that lvalues are copied and rvalues are moved.
You'll organize your code across three files:
Widget.h: Define aWidgetclass that tracks how it receives its data.Your
Widgetshould store astd::stringname and anintvalue. Provide two constructors:- One that takes
const std::string&(lvalue reference) andint— printsWidget constructed (copy): [name] - One that takes
std::string&&(rvalue reference) andint— printsWidget constructed (move): [name]and moves the string
Also add a
display()method that prints[name]: [value].- One that takes
Factory.h: Create your perfect forwarding factory function.Write a template function
make_objectthat accepts a forwarding reference for the name and a regularintfor the value. Usestd::forwardto pass the name argument to theWidgetconstructor while preserving its original value category.The function should return the constructed
Widgetby value.main.cpp: Read two inputs:- A name (string)
- A value (integer)
Demonstrate perfect forwarding by creating two widgets:
- First, store the name in a local variable and pass it to
make_objectas an lvalue - Second, pass a temporary string (the same name concatenated with
"_temp") directly tomake_objectas an rvalue
After creating each widget, call its
display()method.
For example, with inputs Gadget and 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42With inputs Device and 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100The key insight is that your make_object function doesn't know whether it received an lvalue or rvalue—but by using std::forward, it preserves that information when calling the Widget constructor. This allows the constructor overload resolution to select the appropriate version, enabling efficient moves for temporaries while safely copying named variables.
Cheat sheet
Perfect forwarding allows template functions to pass arguments to other functions while preserving their original value category (lvalue or rvalue).
The mechanism uses forwarding references (T&& in template context) combined with std::forward:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}A forwarding reference can bind to both lvalues and rvalues. std::forward<T> conditionally casts the argument back to its original type.
Without std::forward, named parameters inside a function are always treated as lvalues, even if an rvalue was passed. This would prevent move semantics from working correctly.
Example demonstrating value category preservation:
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int n = 10;
wrapper(n); // Calls process(int&) - lvalue preserved
wrapper(20); // Calls process(int&&) - rvalue preservedPerfect forwarding is essential for factory functions, wrapper classes, and generic code that constructs objects or invokes functions while preserving the caller's intent regarding moves versus copies.
Try it yourself
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Read input
string name;
int value;
cin >> name >> value;
// TODO: Create first widget by passing name as an lvalue
// Store the name in a local variable and pass it to make_object
// TODO: Call display() on the first widget
// TODO: Create second widget by passing a temporary string as an rvalue
// Pass (name + "_temp") directly to make_object
// TODO: Call display() on the second widget
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container12Modern C++ Features
Move Semantics & RvaluesPerfect ForwardingLambda Expressions In Depthstd::function & std::bindconstexpr and constevalStructured Bindingsoptional, variant, any