본문으로 건너뛰기 C++ Perfect Forwarding Complete Guide

C++ Perfect Forwarding Complete Guide

C++ Perfect Forwarding Complete Guide

이 글의 핵심

Master C++ perfect forwarding: preserve lvalue/rvalue categories with universal references and std::forward. Complete guide with reference collapsing, factory patterns, and production examples.

Introduction

Perfect Forwarding forwards template function arguments to other functions as-is, preserving lvalue/rvalue characteristics to prevent unnecessary copies. Why needed?:

  • Performance: Eliminate unnecessary copies
  • Type preservation: Maintain lvalue/rvalue characteristics
  • Generality: Works for all types
  • Efficiency: Essential for factory and wrapper functions

1. The Problem

Named Variables Are lvalues

#include <iostream>
// Overloaded process functions
void process(int& x) {
    std::cout << "lvalue version: " << x << std::endl;
}
void process(int&& x) {
    std::cout << "rvalue version: " << x << std::endl;
}
// ❌ Problem: always calls lvalue version
// T&&: universal reference (accepts both lvalue and rvalue)
template<typename T>
void wrapper(T&& arg) {
    // Problem: arg is a named variable
    // Named variables are always treated as lvalue!
    // Even when calling wrapper(20) with rvalue,
    // process(arg) calls lvalue version
    process(arg);  // arg has a name, so it's lvalue!
}
int main() {
    int x = 10;
    wrapper(x);   // lvalue passed → lvalue version (correct)
    wrapper(20);  // rvalue passed → lvalue version (wrong!)
                  // 20 is rvalue but treated as lvalue
    
    return 0;
}

Output:

lvalue version: 10
lvalue version: 20

Problem: wrapper(20) passed rvalue, but process(arg) treats it as lvalue.

2. Perfect Forwarding (std::forward)

#include <iostream>
#include <utility>
void process(int& x) {
    std::cout << "lvalue version: " << x << std::endl;
}
void process(int&& x) {
    std::cout << "rvalue version: " << x << std::endl;
}
// ✅ Solution: use std::forward
template<typename T>
void wrapper(T&& arg) {
    // std::forward<T>(arg): preserve original type characteristics
    // - If arg was passed as lvalue, forward as lvalue
    // - If arg was passed as rvalue, forward as rvalue
    // 
    // How it works:
    // wrapper(x)   → T=int&  → forward returns lvalue
    // wrapper(20)  → T=int   → forward returns rvalue
    process(std::forward<T>(arg));  // Preserve original type
}
int main() {
    int x = 10;
    wrapper(x);   // lvalue passed → lvalue version (correct)
    wrapper(20);  // rvalue passed → rvalue version (correct!)
    
    return 0;
}

Output:

lvalue version: 10
rvalue version: 20

3. Universal Reference (Forwarding Reference)

T&& vs Widget&&

#include <iostream>
class Widget {
public:
    Widget() { std::cout << "Widget created" << std::endl; }
    Widget(const Widget&) { std::cout << "Widget copied" << std::endl; }
    Widget(Widget&&) { std::cout << "Widget moved" << std::endl; }
};
// Universal reference (T&&)
template<typename T>
void func1(T&& arg) {
    std::cout << "Universal reference" << std::endl;
}
// Regular rvalue reference (Widget&&)
void func2(Widget&& arg) {
    std::cout << "rvalue reference" << std::endl;
}
int main() {
    Widget w;
    
    func1(w);         // OK: lvalue
    func1(Widget());  // OK: rvalue
    
    // func2(w);      // Error: lvalue
    func2(Widget());  // OK: rvalue
    
    return 0;
}

Output:

Widget created
Universal reference
Widget created
Widget moved
Universal reference
Widget created
Widget moved
rvalue reference

Key: T&& where T is deduced is a universal reference (also called forwarding reference), binding both lvalue and rvalue. Widget&& is just an rvalue reference.

4. Reference Collapsing

Collapsing Rules

// Reference Collapsing Rules:
// When two references combine, they collapse to one
// 
// T&  &  → T&   (lvalue ref + lvalue ref = lvalue ref)
// T&  && → T&   (lvalue ref + rvalue ref = lvalue ref)
// T&& &  → T&   (rvalue ref + lvalue ref = lvalue ref)
// T&& && → T&&  (rvalue ref + rvalue ref = rvalue ref)
// 
// Key: If any lvalue reference present, result is lvalue reference
template<typename T>
void func(T&& arg) {
    // If T is int&:  int& && → int& (collapsed)
    // If T is int:   int&& (unchanged)
}
int main() {
    int x = 10;
    // lvalue passed: T deduced as int&
    func(x);   // T = int&,  arg = int& && → int& (collapsed)
    
    // rvalue passed: T deduced as int
    func(10);  // T = int,   arg = int&& (unchanged)
    
    return 0;
}

Key: If any lvalue reference present, result is lvalue reference

Type Deduction Process

#include <iostream>
#include <type_traits>
template<typename T>
void test(T&& arg) {
    std::cout << "T is lvalue ref: " 
              << std::is_lvalue_reference_v<T> << std::endl;
    std::cout << "T is rvalue ref: " 
              << std::is_rvalue_reference_v<T> << std::endl;
    std::cout << "arg is lvalue ref: " 
              << std::is_lvalue_reference_v<decltype(arg)> << std::endl;
    std::cout << "arg is rvalue ref: " 
              << std::is_rvalue_reference_v<decltype(arg)> << std::endl;
    std::cout << std::endl;
}
int main() {
    int x = 10;
    
    std::cout << "test(x):" << std::endl;
    test(x);   // T = int&,  arg = int& && → int&
    
    std::cout << "test(10):" << std::endl;
    test(10);  // T = int,   arg = int&&
    
    const int y = 20;
    std::cout << "test(y):" << std::endl;
    test(y);   // T = const int&,  arg = const int&
    
    std::cout << "test(std::move(y)):" << std::endl;
    test(std::move(y));  // T = const int,  arg = const int&&
    
    return 0;
}

5. Practical Examples

Example 1: make_unique Implementation

#include <memory>
#include <iostream>
#include <string>
// my_make_unique: simplified std::make_unique
// T: object type to create
// Args&&...: variadic universal references (perfect forwarding)
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    // std::forward<Args>(args)...: forward each argument preserving type
    // lvalue as lvalue, rvalue as rvalue
    // new T(...): construct T with forwarded arguments
    // wrap in unique_ptr for automatic memory management
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
class Widget {
public:
    Widget(int x, std::string s) : x_(x), s_(s) {
        std::cout << "Widget(" << x_ << ", \"" << s_ << "\")" << std::endl;
    }
    
private:
    int x_;
    std::string s_;
};
int main() {
    std::string str = "test";
    // lvalue passed: str is copied
    auto w1 = my_make_unique<Widget>(10, str);       // lvalue
    
    // rvalue passed: "hello" is moved (temporary string)
    auto w2 = my_make_unique<Widget>(20, "hello");   // rvalue
    
    // Perfect forwarding eliminates unnecessary copies
    
    return 0;
}

Output:

Widget(10, "test")
Widget(20, "hello")

Example 2: Factory Function

#include <memory>
#include <iostream>
#include <string>
template<typename T, typename... Args>
std::shared_ptr<T> createObject(Args&&... args) {
    std::cout << "Creating object..." << std::endl;
    return std::make_shared<T>(std::forward<Args>(args)...);
}
class Person {
public:
    Person(std::string n, int a) : name(n), age(a) {
        std::cout << name << ", " << age << " years old" << std::endl;
    }
    
private:
    std::string name;
    int age;
};
int main() {
    std::string name = "Alice";
    auto p1 = createObject<Person>(name, 25);       // lvalue
    auto p2 = createObject<Person>("Bob", 30);      // rvalue
    auto p3 = createObject<Person>(std::string("Charlie"), 35);  // rvalue
    
    return 0;
}

Output:

Creating object...
Alice, 25 years old
Creating object...
Bob, 30 years old
Creating object...
Charlie, 35 years old

Example 3: Event System

#include <functional>
#include <vector>
#include <iostream>
#include <string>
class EventSystem {
private:
    std::vector<std::function<void()>> handlers;
    
public:
    template<typename Func, typename... Args>
    void registerHandler(Func&& func, Args&&... args) {
        handlers.push_back([
            f = std::forward<Func>(func),
            ... capturedArgs = std::forward<Args>(args)
        ]() mutable {
            f(capturedArgs...);
        });
    }
    
    void trigger() {
        for (auto& handler : handlers) {
            handler();
        }
    }
};
void onEvent(int id, std::string message) {
    std::cout << "Event " << id << ": " << message << std::endl;
}
int main() {
    EventSystem events;
    
    std::string msg = "Hello";
    events.registerHandler(onEvent, 1, msg);       // lvalue
    events.registerHandler(onEvent, 2, "World");   // rvalue
    events.registerHandler(onEvent, 3, std::string("Test"));  // rvalue
    
    std::cout << "Triggering events:" << std::endl;
    events.trigger();
    
    return 0;
}

Output:

Triggering events:
Event 1: Hello
Event 2: World
Event 3: Test

6. Common Issues

Issue 1: Forwarding Without std::forward

#include <iostream>
#include <string>
void process(std::string& s) {
    std::cout << "lvalue: " << s << std::endl;
}
void process(std::string&& s) {
    std::cout << "rvalue: " << s << std::endl;
}
// ❌ Loses move characteristics
template<typename T>
void badWrapper(T&& arg) {
    process(arg);  // Always forwarded as lvalue
}
// ✅ Use forward
template<typename T>
void goodWrapper(T&& arg) {
    process(std::forward<T>(arg));  // Preserve original type
}
int main() {
    std::string s = "test";
    
    std::cout << "badWrapper:" << std::endl;
    badWrapper(s);            // lvalue
    badWrapper("hello");      // lvalue (wrong!)
    
    std::cout << "\ngoodWrapper:" << std::endl;
    goodWrapper(s);           // lvalue
    goodWrapper("world");     // rvalue (correct!)
    
    return 0;
}

Output:

badWrapper:
lvalue: test
lvalue: hello
goodWrapper:
lvalue: test
rvalue: world

Issue 2: Multiple Forwards

#include <iostream>
#include <string>
void process1(std::string s) {
    std::cout << "process1: " << s << std::endl;
}
void process2(std::string s) {
    std::cout << "process2: " << s << std::endl;
}
// ❌ Dangerous: multiple forwards
template<typename T>
void bad(T&& arg) {
    process1(std::forward<T>(arg));  // May move
    process2(std::forward<T>(arg));  // arg already moved!
}
// ✅ Forward only once
template<typename T>
void good(T&& arg) {
    process1(arg);  // Pass as lvalue (copy)
    process2(std::forward<T>(arg));  // Forward only at the end (move)
}
int main() {
    std::string s = "test";
    
    std::cout << "good:" << std::endl;
    good(std::move(s));
    
    return 0;
}

Output:

good:
process1: test
process2: test

Key: Forward the same argument only once. If you need to use it multiple times, pass as lvalue (copy) except for the last use.

Issue 3: auto&& vs T&&

#include <iostream>
#include <string>
class Widget {
public:
    Widget() { std::cout << "Widget()" << std::endl; }
    Widget(const Widget&) { std::cout << "Widget copied" << std::endl; }
    Widget(Widget&&) { std::cout << "Widget moved" << std::endl; }
};
Widget getValue() {
    return Widget();
}
// auto&&: always universal reference
void testAuto() {
    auto&& x = getValue();  // Binds rvalue
    std::cout << "auto&& OK" << std::endl;
}
// T&&: universal reference only in templates
template<typename T>
void testTemplate(T&& arg) {
    std::cout << "T&& OK" << std::endl;
}
// Widget&&: rvalue reference (not universal)
void testWidget(Widget&& arg) {
    std::cout << "Widget&& OK" << std::endl;
}
int main() {
    Widget w;
    
    testAuto();
    
    testTemplate(w);         // OK: lvalue
    testTemplate(Widget());  // OK: rvalue
    
    // testWidget(w);        // Error: lvalue
    testWidget(Widget());    // OK: rvalue
    
    return 0;
}

Key: auto&& is always universal reference. T&& is universal only when T is deduced. Widget&& is just rvalue reference.

7. Production Patterns

Pattern 1: Thread Wrapper

#include <thread>
#include <utility>
#include <iostream>
#include <string>
template<typename Func, typename... Args>
std::thread createThread(Func&& func, Args&&... args) {
    return std::thread(
        std::forward<Func>(func),
        std::forward<Args>(args)...
    );
}
void worker(int id, std::string name) {
    std::cout << "Worker " << id << ": " << name << std::endl;
}
int main() {
    std::string name = "Alice";
    auto t1 = createThread(worker, 1, name);       // lvalue
    auto t2 = createThread(worker, 2, "Bob");      // rvalue
    auto t3 = createThread(worker, 3, std::string("Charlie"));  // rvalue
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

Pattern 2: emplace Implementation

#include <iostream>
#include <vector>
#include <string>
template<typename T>
class MyVector {
private:
    std::vector<T> data_;
    
public:
    template<typename... Args>
    void emplace_back(Args&&... args) {
        data_.emplace_back(std::forward<Args>(args)...);
        std::cout << "emplace_back called" << std::endl;
    }
    
    void print() const {
        for (const auto& item : data_) {
            std::cout << "  " << item << std::endl;
        }
    }
};
int main() {
    MyVector<std::string> vec;
    
    std::string s = "Hello";
    vec.emplace_back(s);              // lvalue (copy)
    vec.emplace_back("World");        // rvalue (move)
    vec.emplace_back(std::string("Test"));  // rvalue (move)
    
    std::cout << "Vector contents:" << std::endl;
    vec.print();
    
    return 0;
}

Output:

emplace_back called
emplace_back called
emplace_back called
Vector contents:
  Hello
  World
  Test

Pattern 3: Logging System

#include <sstream>
#include <iostream>
#include <string>
class Logger {
public:
    template<typename... Args>
    void log(Args&&... args) {
        std::ostringstream oss;
        (oss << ... << std::forward<Args>(args));
        std::cout << "[LOG] " << oss.str() << std::endl;
    }
    
    template<typename... Args>
    void error(Args&&... args) {
        std::ostringstream oss;
        (oss << ... << std::forward<Args>(args));
        std::cerr << "[ERROR] " << oss.str() << std::endl;
    }
};
int main() {
    Logger logger;
    
    std::string user = "Alice";
    logger.log("User ", user, " logged in");  // lvalue
    logger.log("Error code: ", 404);          // rvalue
    logger.error("Failed to connect to ", "database");
    
    return 0;
}

Output:

[LOG] User Alice logged in
[LOG] Error code: 404
[ERROR] Failed to connect to database

8. Advanced Example: Chaining Builder

#include <iostream>
#include <string>
#include <utility>
class QueryBuilder {
    std::string query_;
    
public:
    QueryBuilder() : query_("SELECT * FROM table") {}
    
    template<typename T>
    QueryBuilder&& where(T&& condition) && {
        query_ += " WHERE " + std::forward<T>(condition);
        return std::move(*this);
    }
    
    template<typename T>
    QueryBuilder&& orderBy(T&& field) && {
        query_ += " ORDER BY " + std::forward<T>(field);
        return std::move(*this);
    }
    
    template<typename T>
    QueryBuilder&& limit(T&& count) && {
        query_ += " LIMIT " + std::to_string(std::forward<T>(count));
        return std::move(*this);
    }
    
    std::string build() && {
        return std::move(query_);
    }
};
int main() {
    std::string condition = "age > 18";
    std::string field = "name";
    
    auto query1 = QueryBuilder()
        .where(condition)      // lvalue
        .orderBy(field)        // lvalue
        .limit(10)             // rvalue
        .build();
    
    std::cout << query1 << std::endl;
    
    auto query2 = QueryBuilder()
        .where("status = 'active'")  // rvalue
        .orderBy("created_at")       // rvalue
        .build();
    
    std::cout << query2 << std::endl;
    
    return 0;
}

Output:

SELECT * FROM table WHERE age > 18 ORDER BY name LIMIT 10
SELECT * FROM table WHERE status = 'active' ORDER BY created_at

Summary

Key Points

  1. Perfect forwarding: Preserve lvalue/rvalue characteristics
  2. std::forward: Conditional move
  3. Universal reference: T&& (in templates)
  4. Reference collapsing: lvalue reference takes precedence
  5. Production: Factory, wrapper, emplace patterns

forward vs move

Featurestd::forwardstd::move
PurposeConditional moveUnconditional move
TypePreserve originalCast to rvalue
Use caseTemplatesRegular code
lvaluePreserve lvalueConvert to rvalue
rvaluePreserve rvaluePreserve rvalue

Practical Tips

Usage principles:

  • Factory functions: make_unique, make_shared
  • Wrapper functions: argument forwarding
  • emplace family: emplace_back
  • Event systems: callback registration Performance:
  • Eliminate unnecessary copies
  • Bigger objects = bigger benefit
  • Compile-time optimization
  • No runtime overhead Cautions:
  • Forward only once
  • Understand universal references
  • Reference collapsing rules
  • Perfect forwarding failure cases (bitfields, overloads, brace initialization)

Next Steps


Checklist

Before Writing Code

  • Is this technique the best solution for the current problem?
  • Can team members understand and maintain this code?
  • Does it meet performance requirements?

While Writing Code

  • Have all compiler warnings been resolved?
  • Have edge cases been considered?
  • Is error handling appropriate?

During Code Review

  • Is the code’s intent clear?
  • Are test cases sufficient?
  • Is it documented?

Keywords

C++ perfect forwarding, std::forward, universal reference, forwarding reference, rvalue, templates, move semantics


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Master C++ perfect forwarding: preserve lvalue/rvalue categories with universal references (T&&) and std::forward. Compl…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, perfect forwarding, universal reference, rvalue, std::forward, templates.