C++ Perfect Forwarding | std::forward로 "복사 없이 인자 전달"

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를 올바르게 사용할 수 있습니다.
  • 실전에서 효율적인 템플릿 함수를 작성할 수 있습니다.

목차

  1. 완벽한 전달이란
  2. 문제 시나리오: 왜 Perfect Forwarding이 필요한가
  3. 유니버설 참조
  4. std::forward
  5. 참조 축약 규칙
  6. 완전한 Perfect Forwarding 예제
  7. 자주 발생하는 오류와 해결법
  8. 성능 최적화 팁
  9. 실전 패턴
  10. 프로덕션 패턴

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::threadstd::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를 넘기면 Tint&가 되고 참조 축약으로 argint&가 됩니다. 반면 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를 받는 참조”를 만들기 위해 Tint&로 추론합니다. 그러면 T&&int& &&가 되고, 축약 규칙에 따라 int&가 됩니다. 즉 arg는 lvalue 참조입니다. 반대로 func(10)에서는 10이 rvalue이므로 Tint로 추론되고, 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로

핵심 원칙:

  1. 템플릿 인자는 T&&
  2. 전달 시 std::forward<T>
  3. forward는 한 번만
  4. 반환값에 forward 금지
  5. 여러 인자는 가변 인자 템플릿

자주 묻는 질문 (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에서 람다 활용법