래퍼 함수에서 인자가 복사될 때: 유니버설 참조와 std::forward로 완벽한 전달 구현하기
💡 초보자를 위한 한 줄: 템플릿에서
T&&+std::forward<T>(arg)가 “받은 쪽이 lvalue였으면 lvalue로, rvalue였으면 rvalue로” 넘기는 기본 패턴입니다.std::move와 헷갈리면 move는 항상 rvalue로, forward는 카테고리 유지입니다. 14-1 이동 의미론을 본 뒤 읽으면 연결이 잘 됩니다.
들어가며: 래퍼 함수에서 인자가 복사된다
함수 호출을 로깅하는 래퍼 함수를 만들었습니다. 하지만 인자가 불필요하게 복사되었습니다.
비유하면 완벽한 전달(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를 올바르게 사용할 수 있습니다.
- 실전에서 효율적인 템플릿 함수를 작성할 수 있습니다.
래퍼가 인자의 값 범주를 잃는 문제와 완벽한 전달
문제 상황
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::forwardwrapper2(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
로깅, 팩토리, emplace, 재시도 래퍼에서 생기는 복사
시나리오 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() 반환값이 복사됨
시나리오 5: 재시도 래퍼에서 요청 바디 전달
상황: 네트워크 API를 재시도하는 래퍼 retry(3, sendRequest, body)를 만들었는데, body(std::vector<uint8_t>)를 값으로 받으면 래퍼 진입 시 한 번 복사되고, 대상 함수도 값으로 받으면 시도할 때마다 또 복사됩니다.
여기서 “그럼 Args&& + std::forward로 바꾸면 되겠다”고 생각하기 쉽지만, 재시도는 같은 인자를 여러 번 쓰는 구조라 forward를 루프 안에서 쓰면 오히려 버그가 됩니다. 첫 시도에서 rvalue로 전달된 body가 대상 함수로 이동된 뒤 예외가 나면, 두 번째 시도는 빈 벡터를 보내게 됩니다. 재시도 래퍼에서는 인자를 참조(Args&&)로 받아 래퍼 진입 시 복사를 없애되, 각 시도에서는 lvalue로 넘기고 대상 함수는 const std::vector<uint8_t>&로 받게 하는 것이 맞습니다. 구현은 재시도 래퍼 패턴에 있습니다.
문제 시나리오 시각화
flowchart TB
subgraph bad[❌ 값으로 받는 래퍼]
B1["호출자: rvalue 전달"]
B2["래퍼: Arg arg (복사)"]
B3["내부: arg는 lvalue"]
B4["대상 함수: lvalue 오버로드만 호출"]
B1 --> B2 --> B3 --> B4
end
subgraph good[✅ Perfect Forwarding]
G1["호출자: rvalue 전달"]
G2["래퍼: Arg&& arg (참조)"]
G3["std forward: rvalue로 복원"]
G4["대상 함수: rvalue 오버로드 호출"]
G1 --> G2 --> G3 --> G4
end
T&&가 유니버설 참조가 되는 조건
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 참조만
func3에서도 T는 추론되지만, && 바로 앞이 T 자체가 아니라 std::vector<T>이므로 유니버설 참조가 아닙니다. const T&&도 마찬가지로 rvalue 참조일 뿐입니다. 클래스 템플릿의 멤버 함수 void push(T&& x)도 T가 클래스 인스턴스화 시점에 이미 정해져 있어서 호출할 때 추론이 일어나지 않으므로 rvalue 참조입니다. std::vector<T>::push_back(T&&)가 lvalue를 받지 못하고 const T& 오버로드를 따로 두는 이유가 이것입니다.
| 조건 | 결과 |
|---|---|
template <typename T> void f(T&&) | 유니버설 참조 |
void f(int&&) | rvalue 참조만 |
template <typename T> void f(std::vector<T>&&) | rvalue 참조만 |
template <typename T> void f(const T&&) | rvalue 참조만 |
클래스 템플릿 멤버 void push(T&&) | rvalue 참조만 (T가 이미 고정) |
auto&& x = expr | 유니버설 참조 |
타입 추론 규칙
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면 이동
}
std::forward의 동작 원리와 std::move와의 차이
기본 사용법
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::forward의 동작 원리
std::forward는 특별한 컴파일러 기능이 아니라 static_cast 한 줄입니다. 단순화하면 다음과 같습니다.
// std::forward 단순화된 개념 (lvalue를 받는 버전)
template <typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}
T가int&이면: 반환 타입T&&는 참조 축약으로int&→static_cast<int&>(arg), lvalue 반환T가int이면: 반환 타입은int&&→static_cast<int&&>(arg), rvalue 반환
매개변수 타입이 std::remove_reference_t<T>&로 되어 있는 것이 핵심입니다. 이 형태는 비추론 문맥이라 std::forward(arg)처럼 템플릿 인자를 생략하면 컴파일되지 않고, 반드시 std::forward<T>(arg)로 원래 추론된 T를 명시해야 합니다. 즉 arg가 원래 lvalue였는지 rvalue였는지에 대한 정보는 arg가 아니라 T에 담겨 있고, forward는 그 정보를 꺼내 캐스팅하는 역할만 합니다. 실제 표준 라이브러리에는 rvalue를 받는 두 번째 오버로드도 있는데, rvalue를 lvalue로 forward하려 하면 static_assert로 막아 줍니다.
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
}
| 구분 | std::move | std::forward |
|---|---|---|
| 용도 | 소유권 이전, “더 이상 안 씀” | 래퍼에서 인자 전달 |
| 결과 | 항상 rvalue | 원래가 lvalue면 lvalue, rvalue면 rvalue |
| 사용처 | 이동 생성자, 이동 대입, 멤버로 넘길 때 | 템플릿 래퍼, 팩토리, emplace |
여러 인자 전달
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);
}
문법 설명:
Args&&... args: 인자마다Arg1&& a1, Arg2&& a2, ...로 펼쳐지고, 각각 독립적으로 유니버설 참조가 됩니다. 그래서 lvalue와 rvalue를 섞어 넘겨도 인자별로 따로 추론됩니다.std::forward<Args>(args)...:...이forward호출 전체 뒤에 붙어 있으므로std::forward<Arg1>(a1), std::forward<Arg2>(a2), ...로 펼쳐집니다.std::forward<Args...>(args...)처럼 잘못 쓰면 전혀 다른 뜻이 되어 컴파일 에러가 납니다.
참조 축약 규칙
참조의 참조
// 참조 축약 규칙
// 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
}
로깅 래퍼, make_unique, 스레드 래퍼 전체 코드
예제 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
반환값 전달과 decltype(auto): 이 래퍼는 인자뿐 아니라 반환값도 호출자에게 넘깁니다. decltype(auto)는 return 식의 타입을 decltype 규칙 그대로 반환 타입으로 씁니다. 위 코드처럼 지역 변수 result를 return result;로 반환하면 decltype(result)는 값 타입이라 값으로 반환되고 안전합니다. 하지만 return (result);처럼 괄호를 하나 더 치면 decltype((result))가 되어 지역 변수에 대한 lvalue 참조를 반환하게 되고, 함수가 끝나는 순간 댕글링 참조가 됩니다. 괄호 하나로 의미가 바뀌는 드문 곳이라 리뷰에서도 잘 보이지 않습니다. 대상 함수가 참조를 반환하는 경우(std::vector::operator[] 같은)까지 그대로 전달하고 싶다면 auto result = ...로 받지 말고 return std::forward<Func>(func)(std::forward<Args>(args)...);로 곧바로 반환해야 합니다. 또 func가 void를 반환하면 auto result = ... 줄이 컴파일되지 않으므로, 범용 래퍼라면 if constexpr (std::is_void_v<...>)로 분기가 필요합니다.
예제 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;
}
참고: std::bind는 전달받은 인자를 내부에 저장할 때 lvalue는 복사, rvalue는 이동하지만, 호출할 때는 저장된 값을 lvalue로 넘깁니다. 그래서 void f(std::unique_ptr<int>)처럼 값으로 이동을 받는 함수는 bind로 감싸면 컴파일되지 않습니다. 요즘은 bind 대신 나중에 실행할 콜백 패턴처럼 람다 init-capture를 쓰는 편이 의도가 명확합니다.
예제 4: std::thread 래퍼
#include <string>
#include <thread>
#include <utility>
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) {
// name은 스레드 내부 저장소에서 이동으로 받음
}
int main() {
auto t = makeThread(worker, 1, std::string("Alice"));
t.join();
}
여기서 forward가 해 주는 일은 생각보다 제한적입니다. std::thread 생성자는 받은 인자를 항상 새 스레드용 저장소에 decay-copy(lvalue는 복사, rvalue는 이동)해 두었다가, 새 스레드에서 rvalue로 대상 함수에 넘깁니다. 그래서 forward를 쓰면 “래퍼가 값으로 받으면서 생기는 복사 한 번”이 사라질 뿐, 스레드로 넘어가는 복사/이동 자체는 남습니다. 이 decay-copy 때문에 worker(std::string& out)처럼 참조를 받는 함수에 변수를 넘기면 래퍼를 거치든 안 거치든 컴파일 에러가 나고, std::ref(out)으로 감싸야 합니다. 스레드가 끝나기 전에 원본이 사라지면 안 되는 것도 그때부터는 호출자 책임입니다.
forward 두 번 사용, 반환값 forward, 가변 인자 forward 누락
오류 1: std::forward를 두 번 사용
증상: 호출자가 rvalue를 넘긴 경우, 첫 번째 호출에서 내용이 이동되어 두 번째 호출은 이동된(moved-from) 객체를 받습니다. 표준 타입이라면 UB는 아니지만 빈 문자열·빈 컨테이너 같은 엉뚱한 값이 전달됩니다.
// ❌ 잘못된 코드
template <typename T>
void badWrapper(T&& arg) {
process1(std::forward<T>(arg)); // arg 이동됨
process2(std::forward<T>(arg)); // ❌ 이미 이동된 객체 사용!
}
해결: std::forward는 한 번만, 그리고 마지막 사용에서만 씁니다. 앞선 호출에는 lvalue로 넘깁니다.
// ✅ 올바른 코드: 앞의 호출은 lvalue로, forward는 마지막 사용에서 한 번만
template <typename T>
void goodWrapper(T&& arg) {
process1(arg); // lvalue로 전달 (필요하면 복사)
process2(std::forward<T>(arg)); // 마지막 사용: 이동 가능
}
순서가 핵심입니다. forward를 먼저 하고 뒤에서 arg를 다시 쓰면, lvalue로 테스트할 때는 멀쩡하다가 임시 객체를 넘길 때만 값이 비는 식이라 리뷰와 단위 테스트에서 모두 놓치기 쉽습니다. 컴파일러는 경고하지 않지만 Clang-Tidy의 bugprone-use-after-move가 이 패턴을 잡아 줍니다.
오류 2: 반환값에 std::forward 사용 (댕글링 참조)
증상: 지역 변수나 임시 객체에 대한 참조를 반환하여 댕글링 참조 발생.
// ❌ 위험: 댕글링 참조
template <typename T>
T&& badReturn(T&& arg) {
return std::forward<T>(arg); // 임시 객체면 참조가 무효화됨
}
int main() {
int x = badReturn(42); // OK: 임시 객체가 살아 있는 같은 문장 안에서 복사됨
auto&& r = badReturn(42); // ❌ 문장이 끝나면 42가 파괴됨 → r은 댕글링
int y = r; // undefined behavior!
}
임시 객체는 그 임시를 만든 전체 식이 끝날 때까지만 살아 있습니다. 그래서 반환값을 곧바로 복사하는 첫 줄은 문제가 없고, 참조로 붙잡아 두었다가 다음 문장에서 쓰는 순간 UB가 됩니다. 대부분의 호출에서는 멀쩡하다는 점이 이 버그를 더 찾기 어렵게 만듭니다.
해결: 반환 타입을 값(T)으로 합니다.
// ✅ 올바른 코드: 값 반환
template <typename T>
T goodReturn(T&& arg) {
return std::forward<T>(arg); // lvalue면 복사, rvalue면 이동해서 값으로 반환
}
// ⚠️ decltype(auto)도 참조를 그대로 반환한다
template <typename T>
decltype(auto) carefulReturn(T&& arg) {
return std::forward<T>(arg); // lvalue → T&, rvalue → T&& 반환 (값이 아님!)
}
decltype(auto)는 std::forward<T>(arg) 식의 타입을 그대로 쓰므로, rvalue를 넘기면 int&& 같은 rvalue 참조를 반환합니다. badReturn과 똑같은 댕글링 위험이 있으니, 호출자가 결과를 곧바로 소비하는 것이 보장되는 제네릭 래퍼에서만 쓰고, 그 외에는 값 반환이 안전합니다.
오류 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)...);
}
오류 6: rvalue 참조 매개변수에서 std::move 누락
증상: std::vector<T>&&처럼 추론이 일어나지 않는 rvalue 참조 매개변수(유니버설 참조 절의 표 참고)를 멤버에 저장할 때 복사가 발생합니다.
// ❌ vec는 이름이 있으므로 lvalue → 복사 발생
template <typename T>
class Wrapper {
std::vector<T> data;
public:
Wrapper(std::vector<T>&& vec) : data(vec) {} // 복사!
};
// ✅ 유니버설 참조가 아니므로 forward가 아니라 std::move
template <typename T>
class Wrapper {
std::vector<T> data;
public:
Wrapper(std::vector<T>&& vec) : data(std::move(vec)) {}
};
여기서는 vec가 항상 rvalue에서 왔다는 것이 타입으로 보장되므로 std::forward가 아니라 std::move가 맞습니다. 규칙으로 정리하면 유니버설 참조에는 forward, rvalue 참조에는 move입니다. 반대로 유니버설 참조에 std::move를 쓰면 호출자가 넘긴 lvalue까지 몰래 이동해 버리는 더 나쁜 버그가 됩니다.
오류 요약 표
| 오류 | 원인 | 해결 |
|---|---|---|
| 이중 forward | 같은 인자를 두 번 forward | forward는 마지막 사용에서 한 번만 |
| 댕글링 참조 | T&& 반환 + forward | 값 반환, decltype(auto)는 신중히 |
| const T& + forward | const 참조는 항상 lvalue | T&& 사용 |
| 잘못된 타입 | forward에 추론된 T가 아닌 타입 전달 | forward<T>(arg) |
| forward 누락 | 일부 인자만 forward | Args&&... + forward<Args>(args)... |
| rvalue 참조에서 move 누락 | 이름 있는 rvalue 참조는 lvalue | std::move(vec) |
번외: forward를 빼먹었는데 rvalue 오버로드가 불리는 경우
void process(std::string&) { std::cout << "lvalue\n"; }
void process(std::string&&) { std::cout << "rvalue\n"; }
template <typename T>
void noForward(T&& arg) { process(arg); } // forward 없음
std::string s = "test";
noForward(s); // lvalue
noForward("hello"); // rvalue (!)
forward를 빼먹었으니 둘 다 lvalue가 나올 것 같지만, 두 번째는 rvalue가 출력됩니다. T는 const char(&)[6]로 추론되고, process를 부를 때 문자열 리터럴을 std::string으로 변환하면서 새 임시 객체가 만들어지기 때문입니다. 그 임시가 std::string&&에 바인딩됩니다. “forward를 빼먹으면 항상 lvalue”는 인자 타입과 매개변수 타입이 같을 때만 맞는 말이고, 변환이 끼면 오버로드 선택은 변환 결과(임시 객체)를 기준으로 합니다. 그래서 forward 누락을 테스트로 잡으려면 리터럴 대신 std::string("hello")처럼 매개변수와 같은 타입의 임시를 넘겨야 합니다.
완벽 전달이 실패하는 경우
“완벽” 전달이라는 이름과 달리, 대상 함수를 직접 부르면 되는데 전달 함수를 거치면 안 되는 인자들이 있습니다. 아래 예제의 fwd는 지금까지 본 전형적인 전달 함수입니다. 에러 메시지는 GCC 기준의 예시이며, 컴파일러와 버전에 따라 문구는 다를 수 있습니다.
template <typename F, typename... Args>
void fwd(F&& f, Args&&... args) {
std::forward<F>(f)(std::forward<Args>(args)...);
}
1) 중괄호 초기화 목록
void take(std::vector<int> v);
take({1, 2, 3}); // OK
fwd(take, {1, 2, 3}); // ❌ error: too many arguments to function 'void fwd(F&&, Args&& ...) [with ... Args = {}]'
auto il = {1, 2, 3}; // std::initializer_list<int>로 추론
fwd(take, il); // ✅
템플릿 매개변수는 {1, 2, 3}에서 타입을 추론하지 못합니다(비추론 문맥). 에러 메시지가 “인자가 너무 많다”로 나와서 원인을 떠올리기 어렵다는 점이 특히 불편합니다. auto는 중괄호를 std::initializer_list로 추론하는 예외 규칙이 있으니, 지역 변수에 한 번 담아서 넘기면 됩니다. v.emplace_back({1, 2})가 컴파일되지 않는 것도 같은 이유입니다.
2) 널 포인터로 쓴 0 / NULL
void takePtr(int* p);
takePtr(NULL); // OK (직접 호출에서는 널 포인터로 변환)
fwd(takePtr, NULL); // ❌ error: invalid conversion from 'long long int' to 'int*'
fwd(takePtr, nullptr); // ✅
NULL은 정수 상수라서 전달 함수 안에서는 long이나 long long(플랫폼에 따라 다름) 같은 정수 타입의 변수가 됩니다. 정수 리터럴 0(널 포인터 상수)만 포인터로 암시적 변환되고, 값이 0인 정수 변수는 변환되지 않습니다. 예전 C API 래퍼를 템플릿으로 감쌀 때 가장 먼저 부딪히는 문제이고, 해결은 항상 nullptr입니다.
3) 오버로드된 함수 이름·함수 템플릿 이름
void handle(int);
void handle(double);
fwd(handle, 1); // ❌ no matching function for call to 'fwd(<unresolved overloaded function type>, int)'
fwd(static_cast<void(*)(int)>(handle), 1); // ✅ 어느 오버로드인지 명시
fwd([](auto&&... a) { handle(std::forward<decltype(a)>(a)...); }, 1); // ✅ 람다로 감싸기
직접 호출 handle(1)은 인자를 보고 오버로드를 고르지만, 전달 함수에 이름만 넘기면 F를 추론할 근거가 없습니다. std::thread(handle, 1), std::async, std::invoke도 전부 같은 이유로 실패합니다. static_cast보다 람다로 감싸는 쪽이 나중에 오버로드가 추가돼도 깨지지 않아 유지보수가 편합니다.
4) 비트 필드
struct Header { std::uint32_t version : 4, length : 16; };
void useLen(std::size_t n);
Header h{1, 512};
useLen(h.length); // OK
fwd(useLen, h.length); // ❌ error: cannot bind bit-field 'h.Header::length' to 'unsigned int&'
auto len = static_cast<std::size_t>(h.length); // ✅ 값으로 꺼낸 뒤 전달
fwd(useLen, len);
비트 필드는 주소를 가질 수 없어서 비-const 참조(T&&가 lvalue를 받으면 T&)로 묶을 수 없습니다. 네트워크 패킷 헤더나 레지스터 맵처럼 비트 필드를 쓰는 코드를 로깅 래퍼로 감쌀 때 나오는 에러입니다.
5) 선언만 있는 static const 정수 멤버 (C++14 이전 스타일)
struct Config { static const int MaxRetry = 3; }; // 클래스 안 선언만, 정의 없음
fwd(setRetry, Config::MaxRetry); // 참조로 받으므로 ODR-use → 링크 에러 가능
직접 호출은 값만 쓰므로 정의가 없어도 되지만, 참조로 받으면 실제 메모리 주소가 필요해 “undefined reference” 링크 에러가 날 수 있습니다. 최적화 수준에 따라 나기도 하고 안 나기도 해서 더 헷갈립니다. C++17부터 static constexpr 데이터 멤버는 암시적으로 inline이라 문제가 없고, static const라면 inline static const int MaxRetry = 3;으로 선언하면 됩니다.
| 실패하는 인자 | 원인 | 우회법 |
|---|---|---|
{1, 2, 3} | 중괄호에서 템플릿 타입 추론 불가 | auto il = {...} 후 전달 |
0 / NULL | 정수 타입으로 추론 | nullptr |
| 오버로드된 함수 이름 | 어느 함수인지 추론 불가 | static_cast 또는 람다로 감싸기 |
| 비트 필드 | 참조로 바인딩 불가 | 값으로 복사 후 전달 |
정의 없는 static const 멤버 | 참조가 ODR-use를 일으킴 | static constexpr / inline |
전달 래퍼가 값 복사 래퍼보다 이득인 조건
팁 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));
}
}
팁 5: 값 복사 래퍼와 전달 래퍼의 차이가 커지는 조건
void consume(std::string s);
template <typename Arg>
void badWrapper(Arg arg) {
consume(arg); // 래퍼로 들어올 때 + consume으로 넘길 때 복사
}
template <typename Arg>
void goodWrapper(Arg&& arg) {
consume(std::forward<Arg>(arg)); // rvalue면 이동 1회, lvalue면 복사 1회
}
std::string의 이동은 내부 버퍼 포인터와 크기만 옮기므로 문자열 길이와 무관하게 상수 시간이지만, 복사는 새 버퍼를 할당하고 전체 내용을 복사합니다. 그래서 두 래퍼의 차이는 문자열이 길수록 커지고, 반대로 SSO(짧은 문자열 최적화) 범위 안의 짧은 문자열은 이동도 사실상 바이트 복사라 차이가 거의 없습니다. 실제 수치는 할당기와 데이터 크기에 따라 크게 달라지므로, 중요한 경로라면 직접 측정해 보는 것이 맞습니다. 측정할 때는 인자를 만드는 비용이 타이머 안에 섞이지 않도록 입력을 미리 준비해 두어야 합니다.
| 연산 | 값 복사 래퍼 | Perfect Forwarding 래퍼 | 비고 |
|---|---|---|---|
| 긴 string 전달 | O(n) 복사 + 할당 | O(1) 이동 | SSO 범위의 짧은 문자열은 차이 거의 없음 |
| vector 전달 | 전체 복사 | 포인터만 이동 | 대용량일수록 차이 큼 |
| 팩토리 (임시 인자) | 복사 생성 | 이동 생성 | make_unique 스타일 |
| 작은 트리비얼 타입 | 차이 없음 | 차이 없음 | 값 전달이 더 단순 |
make_unique, emplace_back, 콜백 래퍼 구현
패턴 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);
}
재시도, 메트릭, 캐시, 작업 큐 submit 래퍼
패턴 1: 재시도 래퍼 (Retry Wrapper)
#include <functional>
template <typename Func, typename... Args>
auto retry(int maxAttempts, Func&& func, Args&&... args) {
for (int i = 1; i < maxAttempts; ++i) {
try {
return std::invoke(func, args...); // 재시도할 수 있도록 lvalue로 전달
} catch (...) {
// 로그 후 다음 시도
}
}
// 마지막 시도: 이후 인자를 다시 쓸 일이 없으므로 여기서만 forward
return std::invoke(std::forward<Func>(func), std::forward<Args>(args)...);
}
재시도 래퍼는 같은 인자를 여러 번 쓰므로 루프 안에서 std::forward를 쓰면 안 됩니다(오류 1의 이중 forward와 같은 문제). 첫 시도에서 rvalue 인자가 대상 함수로 이동된 뒤 예외가 나면, 다음 시도는 빈 요청 바디를 보내게 됩니다. 위처럼 앞선 시도는 lvalue로 넘기고 마지막 시도에서만 forward하면 인자를 잃지 않습니다. 마지막 시도의 예외는 잡지 않고 그대로 호출자에게 전파됩니다. 대상 함수가 인자를 값으로 받는다면 시도마다 복사가 생기므로, 재시도 대상 함수는 const T&로 받도록 설계하는 편이 좋습니다.
패턴 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;
}
}
패턴 5: 작업 큐 submit
#include <queue>
#include <mutex>
#include <functional>
#include <utility>
class TaskQueue {
std::queue<std::function<void()>> queue_;
std::mutex mutex_;
public:
template <typename F>
void submit(F&& f) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::forward<F>(f)); // 임시 람다는 이동, 이름 있는 호출체는 복사
}
};
submit([data = std::move(bigVector)] { ... })처럼 임시 람다를 넘기면 캡처된 벡터까지 이동으로 큐에 들어갑니다. 값으로 받았다면(void submit(F f)) 이동이 한 번 더 끼고, const F&로 받았다면 무조건 복사가 됩니다.
패턴 6: 나중에 실행할 콜백 (C++20 팩 init-capture)
이벤트 핸들러나 지연 작업처럼 “인자를 지금 받아 두고 나중에 호출”하는 경우에는 참조를 그대로 들고 있으면 안 됩니다. 호출 시점에 원본이 이미 사라졌을 수 있기 때문입니다. C++20부터는 가변 인자 팩을 람다 init-capture로 한 번에 받을 수 있어서, lvalue는 복사하고 rvalue는 이동해 람다가 값을 소유하게 만들 수 있습니다.
#include <functional>
#include <memory>
#include <string>
#include <utility>
template <typename F, typename... Args>
auto defer(F&& f, Args&&... args) {
return [f = std::forward<F>(f), ...xs = std::forward<Args>(args)]() mutable {
return f(xs...);
};
}
void report(int id, const std::string& msg);
std::string msg = "hello";
auto cb = defer(report, 1, msg); // msg는 복사됨
msg = "changed";
cb(); // "1: hello" — 캡처 시점의 값
auto owned = defer([](std::unique_ptr<int>& p) { /* ... */ }, std::make_unique<int>(42));
owned(); // OK: unique_ptr이 람다 안으로 이동됨
// std::function<void()> fn = std::move(owned); // ❌ 컴파일 에러
C++17까지는 같은 일을 하려면 std::make_tuple(std::forward<Args>(args)...)로 담았다가 std::apply로 풀어야 했습니다. 여기서 흔히 걸리는 함정이 마지막 줄입니다. unique_ptr 같은 이동 전용 값을 캡처하면 람다 자체가 복사 불가능해지는데, std::function은 복사 가능한 호출체만 담을 수 있어서 위 패턴 5의 TaskQueue에 이 람다를 넣으면 std::function 헤더 깊숙한 곳에서 “use of deleted function” 에러가 납니다. 에러 위치가 내 코드가 아니라 표준 라이브러리 내부로 찍혀서 원인을 찾는 데 시간이 걸리는 대표적인 경우입니다. C++23의 std::move_only_function을 쓰거나, 그 전에는 std::shared_ptr로 감싸서 복사 가능하게 만드는 것이 일반적인 해결책입니다.
전달 래퍼 코드 리뷰 체크리스트
-
std::forward는 한 번만 사용 (이중 전달 금지) - 반환 시
T&&대신T또는decltype(auto)검토 - 작은 타입은 값 전달 고려
- 가변 인자 템플릿에서 모든 인자에
forward적용 - 예외 안전성 확인 (RAII, strong guarantee)
- 재시도·다중 호출 래퍼에서는 루프 안에서 forward하지 않기
- Clang-Tidy
bugprone-use-after-move,cppcoreguidelines-missing-std-forward,bugprone-forwarding-reference-overload검사 활용
forward는 한 번만, 반환값 forward 금지, auto&& 남용 주의
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 모두 처리
}
}
std::async로 비동기 실행하기
스레드 생성 래퍼는 std::thread 래퍼 예제에서 다뤘습니다. std::async도 std::thread와 마찬가지로 인자를 decay-copy해서 저장하므로, 참조로 넘기려면 std::ref가 필요합니다.
비동기 실행
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++ 유니버설 레퍼런스
- C++ 참조 축약 규칙
- C++ STL 알고리즘 기초 | sort·find·count·transform·accumulate 가이드
- C++ STL 고급 알고리즘: partition·merge·집합 연산·힙 연산 쓰는 법과 흔한 실수
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
자주 묻는 질문 (FAQ)
Q. 완벽 전달이 실패하는 경우도 있나요?
A. 중괄호 초기화 목록, 널 포인터로 쓴 0/NULL, 오버로드된 함수 이름, 비트 필드, 정의 없는 static const 멤버는 직접 호출은 되지만 전달 함수를 거치면 실패합니다. 완벽 전달이 실패하는 경우에 에러 메시지와 우회법을 정리했습니다.
Q. 같은 인자에 std::forward를 두 번 쓰면 왜 문제가 되나요?
A. rvalue로 들어온 인자는 첫 번째 std::forward에서 이동되어 내용이 비어 있을 수 있습니다. 같은 인자를 다시 forward하면 이미 이동된(moved-from) 객체를 넘기게 되어, 두 번째 호출 대상은 빈 문자열이나 빈 컨테이너를 받습니다. 한 인자를 여러 곳에 써야 한다면 앞선 사용은 일반 lvalue로 참조하고 마지막 사용에서만 std::forward를 적용합니다.