Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 a Widget class that tracks how it receives its data.

    Your Widget should store a std::string name and an int value. Provide two constructors:

    • One that takes const std::string& (lvalue reference) and int — prints Widget constructed (copy): [name]
    • One that takes std::string&& (rvalue reference) and int — prints Widget constructed (move): [name] and moves the string

    Also add a display() method that prints [name]: [value].

  • Factory.h: Create your perfect forwarding factory function.

    Write a template function make_object that accepts a forwarding reference for the name and a regular int for the value. Use std::forward to pass the name argument to the Widget constructor while preserving its original value category.

    The function should return the constructed Widget by value.

  • main.cpp: Read two inputs:
    1. A name (string)
    2. A value (integer)

    Demonstrate perfect forwarding by creating two widgets:

    1. First, store the name in a local variable and pass it to make_object as an lvalue
    2. Second, pass a temporary string (the same name concatenated with "_temp") directly to make_object as 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: 42

With inputs Device and 100:

Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100

The 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 preserved

Perfect 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;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming