[2026] C++ Perfect Forwarding Complete Guide | Universal References & std::forward
이 글의 핵심
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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&&
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Creating object...
Alice, 25 years old
Creating object...
Bob, 30 years old
Creating object...
Charlie, 35 years old
Example 3: Event System
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Triggering events:
Event 1: Hello
Event 2: World
Event 3: Test
6. Common Issues
Issue 1: Forwarding Without std::forward
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
badWrapper:
lvalue: test
lvalue: hello
goodWrapper:
lvalue: test
rvalue: world
Issue 2: Multiple Forwards
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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&&
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
emplace_back called
emplace_back called
emplace_back called
Vector contents:
Hello
World
Test
Pattern 3: Logging System
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
- Perfect forwarding: Preserve lvalue/rvalue characteristics
- std::forward: Conditional move
- Universal reference:
T&&(in templates) - Reference collapsing: lvalue reference takes precedence
- Production: Factory, wrapper, emplace patterns
forward vs move
| Feature | std::forward | std::move |
|---|---|---|
| Purpose | Conditional move | Unconditional move |
| Type | Preserve original | Cast to rvalue |
| Use case | Templates | Regular code |
| lvalue | Preserve lvalue | Convert to rvalue |
| rvalue | Preserve rvalue | Preserve 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
- C++ Universal Reference
- C++ Move Semantics
- C++ Rvalue Reference
Related Articles
- C++ Universal Reference Guide
- C++ Move Semantics | Copy vs Move
- C++ Perfect Forwarding | Pass Arguments Without Copy with std::forward
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