본문으로 건너뛰기
Previous
Next
C++ Universal (Forwarding) References Explained

C++ Universal (Forwarding) References Explained

C++ Universal (Forwarding) References Explained

이 글의 핵심

Forwarding references: when T&& or auto&& deduces to bind both lvalues and rvalues, how they differ from rvalue references, and using std::forward correctly.

What is a universal reference?

Scott Meyers’ term: in template<typename T> void f(T&& x), T&& can bind to lvalues and rvalues because of deduction plus reference collapsing.

template<typename T>
void func(T&& arg) {}
int x = 10;
func(x);              // T = int&
func(10);             // T = int
func(std::move(x));   // T = int

Not universal references

void g(int&&);                    // rvalue only
template<typename T>
struct W { void h(T&&); };        // T is fixed per specialization—not deduced per call like free `T&&`

std::forward

template<typename T>
void wrapper(T&& arg) {
    callee(std::forward<T>(arg));
}

auto&& in range-for

for (auto&& item : container) {
    // binds to either lvalues or rvalues of elements
}

const T&&

This is not a forwarding reference—it binds to rvalues only.

Real-world use cases

Factory functions with perfect forwarding

template<typename T, typename....Args>
std::unique_ptr<T> make_unique_custom(Args&&....args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// Usage
auto p1 = make_unique_custom<std::string>(10, 'x');  // rvalue constructor args
std::string name = "test";
auto p2 = make_unique_custom<std::string>(name);     // lvalue arg

Generic wrapper classes

template<typename Func>
class Timer {
    Func func_;
public:
    template<typename F>
    Timer(F&& f) : func_(std::forward<F>(f)) {}
    
    template<typename....Args>
    auto operator()(Args&&....args) {
        auto start = std::chrono::steady_clock::now();
        auto result = func_(std::forward<Args>(args)...);
        auto end = std::chrono::steady_clock::now();
        std::cout << "Elapsed: " 
                  << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() 
                  << "μs\n";
        return result;
    }
};

Common pitfalls and debugging

Pitfall 1: Forgetting std::forward

template<typename T>
void bad_wrapper(T&& arg) {
    callee(arg);  // ❌ Always passes lvalue, even if arg was rvalue
}
template<typename T>
void good_wrapper(T&& arg) {
    callee(std::forward<T>(arg));  // ✅ Preserves value category
}

Symptom: Move constructors never called, unnecessary copies. Debug tip: Use -Weffc++ or clang-tidy’s modernize-use-std-forward to catch missing forwards.

Pitfall 2: Using std::move instead of std::forward

template<typename T>
void wrong(T&& arg) {
    callee(std::move(arg));  // ❌ Forces rvalue even for lvalue inputs
}

Result: Lvalue arguments get moved-from unexpectedly, causing bugs.

Pitfall 3: Deduced auto&& in wrong context

auto&& x = get_value();  // OK: binds to return value
x.modify();              // May be dangling if get_value() returns prvalue and no lifetime extension

Safe pattern: Use auto&& in range-for or when you immediately consume the value.

Compiler behavior across versions

CompilerVersionForwarding reference supportNotes
GCC4.8+Full C++11 supportEarly versions had bugs with nested templates
Clang3.3+Full supportBetter error messages for deduction failures
MSVC2015+Full support2013 had partial support with workarounds needed
C++20 improvement: Concepts can constrain forwarding references more clearly:
template<std::movable T>
void process(T&& arg) {
    // T&& here is still a forwarding reference, but constrained
}

Performance considerations

Zero overhead: Forwarding references with std::forward compile to the same assembly as hand-written overloads (lvalue/rvalue pairs), but with less code duplication. Benchmark (GCC 13, -O3):

// Hand-written overloads: 2 functions
void process(Widget& w);        // lvalue
void process(Widget&& w);       // rvalue
// Forwarding reference: 1 template
template<typename T>
void process(T&& w) { /* ....*/ }

Both produce identical assembly for process(widget) and process(std::move(widget)) calls. When forwarding references add overhead: If the template instantiates many times with different types, code size increases. Use explicit overloads for hot paths with few types.

Debugging forwarding reference deduction

template<typename T>
void debug_type(T&& arg) {
    // Force compile error to see T
    typename T::intentional_error;
}
int x = 10;
debug_type(x);        // Error shows T = int&
debug_type(10);       // Error shows T = int

Runtime type inspection

#include <typeinfo>
#include <iostream>
template<typename T>
void show_type(T&& arg) {
    std::cout << "T = " << typeid(T).name() << "\n";
    std::cout << "T&& = " << typeid(decltype(arg)).name() << "\n";
}

Note: typeid output is mangled; use __cxxabiv1::__cxa_demangle on GCC/Clang or boost::core::demangle.

Keywords

C++, forwarding reference, universal reference, std::forward, templates, perfect forwarding, value categories


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Forwarding references: when T&& or auto&& deduces to bind both lvalues and rvalues, how they differ from rvalue referenc… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, universal-reference, forwarding-reference, C++11 등으로 검색하시면 이 글이 도움이 됩니다.