C++ Perfect Forwarding | std::forward로 "복사 없이 인자 전달"
이 글의 핵심
C++ Perfect Forwarding에 대한 실전 가이드입니다. std::forward로 등을 예제와 함께 상세히 설명합니다.
들어가며: 래퍼 함수에서 인자가 복사된다
”함수를 감싸면 성능이 떨어져요”
함수 호출을 로깅하는 래퍼 함수를 만들었습니다. 하지만 인자가 불필요하게 복사되었습니다.
비유하면 완벽한 전달(perfect forwarding)은 “택배 상자가 원래 배송 옵션(일반/급송)을 유지한 채로 다음 거점에 넘어가는 것”처럼, lvalue로 받으면 lvalue로, rvalue로 받으면 rvalue로 그 성질을 유지해 넘기는 기법입니다. Perfect forwarding은 “래퍼가 받은 인자를 내부 함수에 lvalue/rvalue 성질을 유지한 채 그대로 넘기는” 기법입니다. 템플릿에서 T&&(유니버설 참조—템플릿 인자 T에 대해 lvalue면 lvalue 참조, rvalue면 rvalue 참조로 추론되는 참조)와 std::forward를 쓰면, 호출자가 lvalue를 넘기면 lvalue로, rvalue를 넘기면 rvalue로 전달되어 불필요한 복사와 이동이 사라집니다. 팩토리·래퍼·emplace 스타일 API를 만들 때 실무에서 자주 쓰입니다.
문제의 코드:
template <typename Func, typename Arg>
void logAndCall(Func func, Arg arg) { // ❌ arg가 복사됨
std::cout << "Calling function\n";
func(arg);
}
void process(std::string str) {
std::cout << "Processing: " << str << "\n";
}
int main() {
std::string text = "Hello";
logAndCall(process, text); // text가 2번 복사됨!
}
Perfect Forwarding으로 해결 (아래는 복사해 붙여넣은 뒤 g++ -std=c++17 -o forward forward.cpp && ./forward 로 실행 가능):
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o forward forward.cpp && ./forward
#include <iostream>
#include <string>
#include <utility>
void process(const std::string& s) { std::cout << "lvalue: " << s << "\n"; }
void process(std::string&& s) { std::cout << "rvalue: " << s << "\n"; }
template <typename Func, typename Arg>
void logAndCall(Func func, Arg&& arg) {
std::cout << "Calling function\n";
func(std::forward<Arg>(arg));
}
int main() {
std::string text = "Hello";
logAndCall(process, text); // lvalue 전달
logAndCall(process, std::string("Hi")); // rvalue 전달
return 0;
}
실행 결과: Calling function, lvalue: Hello, Calling function, rvalue: Hi 가 순서대로 출력됩니다.
이 글을 읽으면:
- 완벽한 전달의 개념을 이해할 수 있습니다.
- 유니버설 참조(universal reference)를 사용할 수 있습니다.
- std::forward를 올바르게 사용할 수 있습니다.
- 실전에서 효율적인 템플릿 함수를 작성할 수 있습니다.
목차
- 완벽한 전달이란
- 문제 시나리오: 왜 Perfect Forwarding이 필요한가
- 유니버설 참조
- std::forward
- 참조 축약 규칙
- 완전한 Perfect Forwarding 예제
- 자주 발생하는 오류와 해결법
- 성능 최적화 팁
- 실전 패턴
- 프로덕션 패턴
1. 완벽한 전달이란
문제 상황
process는 lvalue 오버로드와 rvalue 오버로드가 따로 있습니다. **wrapper1(T arg)**처럼 값으로 받으면, 호출자가 wrapper1(20)으로 rvalue를 넘겨도 arg는 복사된 lvalue가 됩니다. 그래서 내부에서 process(arg)를 호출하면 항상 lvalue 버전만 불리고, rvalue로 넘겼을 때의 최적화(이동)를 살리지 못합니다. “래퍼를 거치면 값의 성질(lvalue/rvalue)이 사라진다”가 문제입니다.
void process(int& x) {
std::cout << "lvalue: " << x << "\n";
}
void process(int&& x) {
std::cout << "rvalue: " << x << "\n";
}
// ❌ 나쁜 래퍼
template <typename T>
void wrapper1(T arg) {
process(arg); // 항상 lvalue로 전달됨
}
int main() {
int a = 10;
wrapper1(a); // lvalue: 10
wrapper1(20); // lvalue: 20 (❌ rvalue여야 함!)
}
완벽한 전달
**T&&는 템플릿에서 타입 추론이 일어날 때 “유니버설 참조”가 되어, lvalue가 오면 lvalue 참조로, rvalue가 오면 rvalue 참조로 추론됩니다. std::forward<T>(arg)**는 그 “원래 성질”을 복원해서 다음 함수에 넘기므로, wrapper2(a)는 lvalue로, wrapper2(20)은 rvalue로 process가 호출됩니다. 이렇게 래퍼를 거쳐도 lvalue/rvalue가 유지되는 것이 완벽한 전달(perfect forwarding)입니다.
// ✅ 완벽한 전달
template <typename T>
void wrapper2(T&& arg) {
process(std::forward<T>(arg)); // lvalue면 lvalue로, rvalue면 rvalue로
}
int main() {
int a = 10;
wrapper2(a); // lvalue: 10
wrapper2(20); // rvalue: 20 ✅
}
완벽한 전달 흐름도
flowchart LR
subgraph caller["호출자"]
A1["lvalue 전달"]
A2["rvalue 전달"]
end
subgraph wrapper["래퍼 T&&"]
B1["T = int&"]
B2["T = int"]
end
subgraph forward["std forward"]
C1["lvalue로 전달"]
C2["rvalue로 전달"]
end
subgraph target["대상 함수"]
D1["lvalue 오버로드"]
D2["rvalue 오버로드"]
end
A1 --> B1 --> C1 --> D1
A2 --> B2 --> C2 --> D2
2. 문제 시나리오: 왜 Perfect Forwarding이 필요한가
시나리오 1: 로깅 래퍼에서 대용량 객체 복사
상황: API 호출을 로깅하는 래퍼를 만들었는데, std::vector<LargeData>를 넘길 때마다 전체 복사가 발생합니다.
// ❌ 문제: 1MB 데이터가 매 호출마다 복사됨
template <typename Func, typename Arg>
void logAndCall(Func func, Arg arg) {
log("Calling API");
func(arg); // arg는 항상 lvalue → 복사
}
void processData(std::vector<LargeData> data); // 이동 가능한데 복사됨
std::vector<LargeData> data = loadData();
logAndCall(processData, std::move(data)); // std::move해도 래퍼에서 복사!
해결: Arg&&와 std::forward로 rvalue를 그대로 전달하면 이동만 발생합니다.
// ✅ 해결: rvalue면 이동, lvalue면 참조
template <typename Func, typename Arg>
void logAndCall(Func func, Arg&& arg) {
log("Calling API");
func(std::forward<Arg>(arg));
}
시나리오 2: 팩토리 함수에서 생성자 인자 전달
상황: make_unique처럼 객체를 생성하는 팩토리에서, 생성자에 lvalue/rvalue를 그대로 넘겨야 합니다.
// ❌ 문제: const T&로 받으면 항상 복사
template <typename T, typename Arg>
std::unique_ptr<T> badMakeUnique(const Arg& arg) {
return std::unique_ptr<T>(new T(arg)); // 항상 복사 생성자 호출
}
struct Widget {
Widget(std::string name); // 이동 생성자도 있는데 복사만 됨
};
auto w = badMakeUnique<Widget>(getTemporaryString()); // 불필요한 복사
해결: Args&&...와 std::forward<Args>(args)...로 완벽 전달합니다.
// ✅ 해결
template <typename T, typename... Args>
std::unique_ptr<T> myMakeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
시나리오 3: emplace 스타일 API
상황: vector::push_back(T&&)는 이동을 지원하지만, emplace_back은 생성자 인자를 직접 전달해 컨테이너 내부에서 객체를 생성합니다. 이때 인자의 값 카테고리를 유지해야 합니다.
// ❌ push_back은 객체를 받음 (이미 생성된 객체)
vec.push_back(Widget(1, "a")); // 임시 객체 생성 → 이동
// ✅ emplace_back은 생성자 인자를 전달 (내부에서 생성)
vec.emplace_back(1, "a"); // 복사/이동 없이 직접 생성
emplace_back 내부 구현이 perfect forwarding을 사용하지 않으면, std::string 같은 인자가 불필요하게 복사됩니다.
시나리오 4: 스레드/비동기 래퍼
상황: std::thread나 std::async에 인자를 넘길 때, rvalue는 이동되어야 하고 lvalue는 복사되어야 합니다. 래퍼를 만들면 이 구분이 깨질 수 있습니다.
// ❌ 래퍼가 값으로 받으면 복사
template <typename Func, typename Arg>
std::thread badMakeThread(Func func, Arg arg) {
return std::thread(func, arg); // arg가 복사됨
}
void worker(std::string config); // config는 10KB
badMakeThread(worker, loadConfig()); // loadConfig() 반환값이 복사됨
3. 유니버설 참조
T&& vs 유니버설 참조
**T&&**가 “유니버설 참조”가 되는 것은 타입 T가 그 자리에서 추론될 때만입니다. func1(T&& arg)에서는 T가 호출 시점에 추론되므로 lvalue를 넘기면 T가 int&가 되고 참조 축약으로 arg는 int&가 됩니다. 반면 func2(int&& arg)나 func3(std::vector<T>&& arg)처럼 타입이 이미 정해져 있으면 “rvalue 참조만 받는” 일반 참조이므로, 유니버설 참조가 아닙니다.
// 유니버설 참조 (타입 추론 발생)
template <typename T>
void func1(T&& arg); // ✅ 유니버설 참조
// rvalue 참조 (타입 고정)
void func2(int&& arg); // ❌ rvalue 참조만
// rvalue 참조 (타입 고정)
template <typename T>
void func3(std::vector<T>&& arg); // ❌ rvalue 참조만
타입 추론 규칙
template <typename T>
void func(T&& arg);
int x = 10;
func(x); // T = int&, arg = int& && → int&
func(10); // T = int, arg = int&&
auto&&도 유니버설 참조
int x = 10;
auto&& a = x; // int&
auto&& b = 10; // int&&
// 범위 기반 for에서 유용
std::vector<std::string> vec = {"a", "b", "c"};
for (auto&& item : vec) {
// lvalue면 참조, rvalue면 이동
}
4. std::forward
기본 사용법
template <typename T>
void wrapper(T&& arg) {
// std::forward: arg를 원래 타입으로 전달
process(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // T = int&, forward<int&>(arg) → lvalue
wrapper(20); // T = int, forward<int>(arg) → rvalue
}
std::move vs std::forward
// std::move: 항상 rvalue로
std::string str = "Hello";
process(std::move(str)); // 항상 rvalue
// std::forward: 조건부
template <typename T>
void func(T&& arg) {
process(std::forward<T>(arg)); // lvalue면 lvalue, rvalue면 rvalue
}
여러 인자 전달
template <typename Func, typename... Args>
void callFunction(Func func, Args&&... args) {
func(std::forward<Args>(args)...);
}
void process(int a, std::string b, double c) {
std::cout << a << ", " << b << ", " << c << "\n";
}
int main() {
callFunction(process, 42, std::string("hello"), 3.14);
}
5. 참조 축약 규칙
참조의 참조
// 참조 축약 규칙
// T& & → T&
// T& && → T&
// T&& & → T&
// T&& && → T&&
template <typename T>
void func(T&& arg);
int x = 10;
// func(x): T = int&
// arg의 타입 = int& && → int& (축약)
// func(10): T = int
// arg의 타입 = int&&
단계별 이해: func(x)를 호출하면 x는 lvalue이므로 컴파일러는 “lvalue를 받는 참조”를 만들기 위해 T를 int&로 추론합니다. 그러면 T&&는 int& &&가 되고, 축약 규칙에 따라 int&가 됩니다. 즉 arg는 lvalue 참조입니다. 반대로 func(10)에서는 10이 rvalue이므로 T는 int로 추론되고, T&&는 int&& 그대로라 arg는 rvalue 참조가 됩니다. 그래서 std::forward<T>(arg)가 “원래 lvalue였으면 lvalue로, rvalue였으면 rvalue로” 다시 넘길 수 있습니다.
실제 예제
template <typename T>
void test(T&& arg) {
using RawType = std::remove_reference_t<T>;
if constexpr (std::is_lvalue_reference_v<T>) {
std::cout << "lvalue reference\n";
} else {
std::cout << "rvalue reference\n";
}
}
int main() {
int x = 10;
test(x); // lvalue reference
test(10); // rvalue reference
}
6. 완전한 Perfect Forwarding 예제
예제 1: 실행 가능한 로깅 래퍼 (전체 코드)
#include <iostream>
#include <string>
#include <utility>
#include <chrono>
// 대상 함수들: lvalue/rvalue 오버로드
void process(const std::string& s) {
std::cout << " [lvalue] " << s << "\n";
}
void process(std::string&& s) {
std::cout << " [rvalue] " << s << " (이동 가능)\n";
}
// Perfect forwarding 래퍼
template <typename Func, typename... Args>
decltype(auto) logAndCall(const char* name, Func&& func, Args&&... args) {
std::cout << ">>> " << name << " 호출\n";
auto start = std::chrono::high_resolution_clock::now();
auto result = std::forward<Func>(func)(std::forward<Args>(args)...);
auto end = std::chrono::high_resolution_clock::now();
auto us = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
std::cout << "<<< " << name << " 완료 (" << us << " us)\n";
return result;
}
int main() {
std::string text = "Hello";
logAndCall("process(lvalue)", process, text);
logAndCall("process(rvalue)", process, std::string("World"));
return 0;
}
실행: g++ -std=c++17 -O2 -o log_wrapper log_wrapper.cpp && ./log_wrapper
예제 2: make_unique 완전 구현
#include <memory>
#include <utility>
template <typename T, typename... Args>
std::unique_ptr<T> myMakeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
struct Widget {
int id;
std::string name;
Widget(int i, std::string n) : id(i), name(std::move(n)) {}
};
int main() {
// lvalue 전달
std::string name = "Widget1";
auto w1 = myMakeUnique<Widget>(1, name);
// rvalue 전달 (이동)
auto w2 = myMakeUnique<Widget>(2, std::string("Widget2"));
// 임시 객체
auto w3 = myMakeUnique<Widget>(3, "Widget3");
}
예제 3: 스레드 풀 작업 큐에 인자 전달
#include <functional>
#include <future>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
template <typename Func, typename... Args>
auto enqueue(Func&& func, Args&&... args)
-> std::future<std::invoke_result_t<Func, Args...>>
{
using ReturnType = std::invoke_result_t<Func, Args...>;
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<Func>(func), std::forward<Args>(args)...)
);
std::future<ReturnType> result = task->get_future();
// ... 큐에 추가 ...
return result;
}
7. 자주 발생하는 오류와 해결법
오류 1: std::forward를 두 번 사용
증상: 첫 번째 호출 후 객체가 이동되어 비어 있고, 두 번째 호출에서 undefined behavior 발생.
// ❌ 잘못된 코드
template <typename T>
void badWrapper(T&& arg) {
process1(std::forward<T>(arg)); // arg 이동됨
process2(std::forward<T>(arg)); // ❌ 이미 이동된 객체 사용!
}
해결: std::forward는 한 번만 사용합니다. 여러 함수에 전달해야 하면 첫 번째만 forward하고 나머지는 lvalue로 전달하거나, 복사가 필요한 설계로 변경합니다.
// ✅ 올바른 코드
template <typename T>
void goodWrapper(T&& arg) {
process1(std::forward<T>(arg)); // 이동 또는 참조
process2(arg); // lvalue로 전달 (이미 소비된 경우 주의)
}
오류 2: 반환값에 std::forward 사용 (댕글링 참조)
증상: 지역 변수나 임시 객체에 대한 참조를 반환하여 댕글링 참조 발생.
// ❌ 위험: 댕글링 참조
template <typename T>
T&& badReturn(T&& arg) {
return std::forward<T>(arg); // 임시 객체면 참조가 무효화됨
}
int main() {
int x = badReturn(42); // undefined behavior!
}
해결: 반환 타입을 값(T)으로 하고 std::forward로 전달하거나, decltype(auto)를 신중히 사용합니다.
// ✅ 올바른 코드
template <typename T>
T goodReturn(T&& arg) {
return std::forward<T>(arg); // 값 반환
}
오류 3: const T&로 받아서 forward
증상: const T&는 항상 lvalue이므로 std::forward해도 rvalue로 전달되지 않습니다.
// ❌ const 참조는 lvalue만 받음
template <typename T>
void badForward(const T& arg) {
process(std::forward<const T&>(arg)); // 항상 lvalue
}
해결: 유니버설 참조 T&&를 사용합니다.
// ✅ 올바른 코드
template <typename T>
void goodForward(T&& arg) {
process(std::forward<T>(arg));
}
오류 4: std::forward에 잘못된 타입 전달
증상: std::forward<Arg>(arg)에서 Arg가 실제 인자 타입과 맞지 않으면 잘못된 캐스팅이 됩니다.
// ❌ 잘못된 타입
template <typename T>
void wrapper(T&& arg) {
process(std::forward<int>(arg)); // ❌ T가 아닌 int 사용
}
해결: std::forward에는 반드시 추론된 템플릿 파라미터 타입을 사용합니다.
// ✅ 올바른 코드
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
오류 5: 가변 인자에서 forward 누락
증상: 일부 인자만 std::forward하고 나머지는 값으로 전달하면 불필요한 복사 발생.
// ❌ 일부만 forward
template <typename Func, typename Arg1, typename Arg2>
void badCall(Func func, Arg1&& a1, Arg2 a2) { // a2가 값으로 복사됨
func(std::forward<Arg1>(a1), a2);
}
해결: 전달할 모든 인자에 Args&&와 std::forward<Args>(args)...를 적용합니다.
// ✅ 올바른 코드
template <typename Func, typename... Args>
void goodCall(Func func, Args&&... args) {
func(std::forward<Args>(args)...);
}
8. 성능 최적화 팁
팁 1: 불필요한 perfect forwarding 피하기
작은 타입(int, double, 포인터 등)은 값으로 전달하는 것이 더 빠를 수 있습니다. 복사 비용이 거의 없고, 참조로 받으면 간접 접근 오버헤드가 생깁니다.
// 작은 타입: 값 전달이 나을 수 있음
template <typename T>
void processSmall(int id, T&& largeObj) { // id는 값, largeObj는 forward
// ...
}
팁 2: 이동 가능한 큰 객체는 rvalue로 받기
std::vector, std::string 등 이동 가능한 타입은 perfect forwarding으로 rvalue 경로를 확보하면 이동만 발생합니다.
// ✅ vector, string 등: T&& + forward로 이동 활용
template <typename T>
void addToCache(T&& key) {
cache_.insert(std::forward<T>(key));
}
팁 3: emplace vs push_back
| 방식 | 복사/이동 횟수 | 비고 |
|---|---|---|
push_back(T(x)) | 생성 1회 + 이동 1회 | 임시 생성 후 이동 |
push_back(std::move(x)) | 이동 1회 | 기존 객체 이동 |
emplace_back(args...) | 생성 1회 (내부) | 인자만 전달, 최소 비용 |
std::vector<std::pair<int, std::string>> vec;
// push_back: 임시 pair 생성 → 이동
vec.push_back({1, "a"});
// emplace_back: 인자만 전달, 내부에서 직접 생성
vec.emplace_back(1, "a"); // ✅ 더 효율적
팁 4: SFINAE로 무거운 타입만 forward
타입 특성에 따라 전략을 나눌 수 있습니다.
template <typename T>
void process(T&& arg) {
if constexpr (std::is_trivially_copyable_v<std::remove_reference_t<T>> &&
sizeof(T) <= sizeof(void*)) {
// 작은 타입: 값으로 전달
doProcess(arg);
} else {
// 큰 타입: forward
doProcess(std::forward<T>(arg));
}
}
9. 실전 패턴
패턴 1: make_unique 구현
template <typename T, typename... Args>
std::unique_ptr<T> myMakeUnique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
int main() {
auto p = myMakeUnique<Point>(10, 20);
}
패턴 2: emplace_back 구현
template <typename T>
class MyVector {
T* data;
size_t size;
size_t capacity;
public:
template <typename... Args>
void emplace_back(Args&&... args) {
if (size >= capacity) {
// 재할당...
}
new (&data[size]) T(std::forward<Args>(args)...);
++size;
}
};
패턴 3: 팩토리 함수
template <typename T, typename... Args>
T create(Args&&... args) {
std::cout << "Creating object...\n";
return T(std::forward<Args>(args)...);
}
struct Widget {
int value;
std::string name;
Widget(int v, std::string n) : value(v), name(std::move(n)) {}
};
int main() {
auto w = create<Widget>(42, "test");
}
패턴 4: 콜백 래퍼
template <typename Func, typename... Args>
auto measureTime(Func&& func, Args&&... args) {
auto start = std::chrono::high_resolution_clock::now();
auto result = std::forward<Func>(func)(std::forward<Args>(args)...);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Time: " << duration.count() << " us\n";
return result;
}
int compute(int a, int b) {
return a + b;
}
int main() {
int result = measureTime(compute, 3, 5);
}
패턴 5: 조건부 전달
template <typename T>
void processValue(T&& value) {
if constexpr (std::is_lvalue_reference_v<T>) {
// lvalue: 참조로 처리
std::cout << "Processing lvalue\n";
doSomething(value);
} else {
// rvalue: 이동으로 처리
std::cout << "Processing rvalue\n";
doSomething(std::move(value));
}
}
패턴 6: 멤버 함수 전달
class Logger {
public:
template <typename Func, typename... Args>
auto logCall(const char* name, Func&& func, Args&&... args) {
std::cout << "Calling " << name << "\n";
auto result = std::forward<Func>(func)(std::forward<Args>(args)...);
std::cout << "Finished " << name << "\n";
return result;
}
};
int add(int a, int b) {
return a + b;
}
int main() {
Logger logger;
int result = logger.logCall("add", add, 3, 5);
}
10. 프로덕션 패턴
패턴 1: 재시도 래퍼 (Retry Wrapper)
template <typename Func, typename... Args>
auto retry(int maxAttempts, Func&& func, Args&&... args) {
std::exception_ptr lastError;
for (int i = 0; i < maxAttempts; ++i) {
try {
return std::forward<Func>(func)(std::forward<Args>(args)...);
} catch (...) {
lastError = std::current_exception();
if (i == maxAttempts - 1) std::rethrow_exception(lastError);
}
}
std::rethrow_exception(lastError);
}
패턴 2: 메트릭 수집 래퍼
template <typename Func, typename... Args>
auto withMetrics(const char* name, Func&& func, Args&&... args) {
auto start = std::chrono::steady_clock::now();
auto result = std::forward<Func>(func)(std::forward<Args>(args)...);
auto elapsed = std::chrono::steady_clock::now() - start;
metrics::record(name, elapsed);
return result;
}
패턴 3: 스레드 안전 캐시 (getOrCreate)
template <typename Key, typename Factory>
auto getOrCreate(Key&& key, Factory&& factory) {
std::lock_guard lock(mutex_);
auto it = cache_.find(key);
if (it == cache_.end()) {
it = cache_.emplace(
std::forward<Key>(key),
std::forward<Factory>(factory)()
).first;
}
return it->second;
}
패턴 4: 옵션/에러 전파 래퍼
template <typename Func, typename... Args>
std::optional<std::invoke_result_t<Func, Args...>>
tryCall(Func&& func, Args&&... args) {
try {
return std::forward<Func>(func)(std::forward<Args>(args)...);
} catch (...) {
return std::nullopt;
}
}
프로덕션 체크리스트
-
std::forward는 한 번만 사용 (이중 전달 금지) - 반환 시
T&&대신T또는decltype(auto)검토 - 작은 타입은 값 전달 고려
- 가변 인자 템플릿에서 모든 인자에
forward적용 - 예외 안전성 확인 (RAII, strong guarantee)
주의사항
forward는 한 번만
template <typename T>
void func(T&& arg) {
process1(std::forward<T>(arg));
// process2(std::forward<T>(arg)); // ❌ 위험: 이미 이동됨
process2(arg); // ✅ lvalue로 전달
}
반환값에 forward 사용 금지
template <typename T>
T&& badFunction(T&& arg) {
return std::forward<T>(arg); // ❌ 댕글링 참조!
}
template <typename T>
T goodFunction(T&& arg) {
return std::forward<T>(arg); // ✅ 값 반환
}
auto&& 남용 금지
// ❌ 불필요
auto&& x = 42;
// ✅ 명확한 타입
int x = 42;
// ✅ auto&& 유용한 경우
template <typename T>
void func(T&& container) {
for (auto&& item : container) {
// lvalue/rvalue 모두 처리
}
}
실전 예제
스레드 생성
template <typename Func, typename... Args>
std::thread makeThread(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 << "\n";
}
int main() {
auto t = makeThread(worker, 1, "Alice");
t.join();
}
비동기 실행
template <typename Func, typename... Args>
auto asyncCall(Func&& func, Args&&... args) {
return std::async(std::launch::async,
std::forward<Func>(func),
std::forward<Args>(args)...);
}
int compute(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return a + b;
}
int main() {
auto future = asyncCall(compute, 3, 5);
std::cout << "Result: " << future.get() << "\n";
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
- C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법
- C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
이 글에서 다루는 키워드 (관련 검색어)
C++ perfect forwarding, std::forward, 유니버설 참조, T&&, 참조 축약, emplace_back 원리 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 설명 |
|---|---|
| 유니버설 참조 | T&& (타입 추론 시) |
| std::forward | 조건부 캐스팅 |
| std::move | 무조건 rvalue 캐스팅 |
| 참조 축약 | T& && → T& |
| 완벽한 전달 | lvalue는 lvalue로, rvalue는 rvalue로 |
핵심 원칙:
- 템플릿 인자는
T&& - 전달 시
std::forward<T> - forward는 한 번만
- 반환값에 forward 금지
- 여러 인자는 가변 인자 템플릿
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 래퍼 함수, 팩토리 함수, emplace_back 스타일 API, 스레드/비동기 래퍼, 로깅·재시도·메트릭 수집 등 “인자를 그대로 넘겨야 하는” 모든 상황에서 perfect forwarding이 필요합니다. 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: std::forward로 값 카테고리를 유지한 채 인자를 전달할 수 있습니다. 다음으로 프로파일링(#15-1)를 읽어보면 좋습니다.
이전 글: [C++ 실전 가이드 #14-1] Move Semantics와 rvalue 참조: 불필요한 복사 제거하기
다음 글: [C++ 실전 가이드 #15-1] 프로파일링과 병목 지점 찾기: 성능 측정의 기초
관련 글
- C++ Perfect Forwarding 완벽 가이드 | 유니버설 참조·std::forward
- C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드
- C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
- C++ STL 고급 알고리즘 | partition·merge·집합 연산·힙으로 데이터 처리 마스터
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법