[C++] Perfect forwarding
Perfect forwarding은 원본 함수(func)와 원본 함수의 인자를 인자로 받는 래퍼 함수(templateFunc)가 있을 때, 원본 함수의 인자를 완벽하게 전달하는 것을 의미한다.
perfect forwarding이 무엇인지 확인하기 전에 다음과 같은 상황을 먼저 확인해보자.
#include <iostream>
template <typename T>
void templateFunc(T param) {
func(param);
}
class Base {
public:
Base() = default;
};
void func(Base& param) {
std::cout << "lvalue reference" << std::endl;
}
void func(const Base& param) {
std::cout << "lvalue constant reference" << std::endl;
}
void func(Base&& param) {
std::cout << "rvalue reference" << std::endl;
}
int main() {
Base b;
const Base cb;
std::cout << "=========Func=========" << std::endl;
func(b);
func(cb);
func(Base());
std::cout << "=====TemplateFunc=====" << std::endl;
templateFunc(b);
templateFunc(cb);
templateFunc(Base());
return 0;
}
=========Func=========
lvalue reference
lvalue constant reference
rvalue reference
=====TemplateFunc=====
lvalue reference
lvalue reference
lvalue reference
func 함수를 곧바로 호출한 경우에는 각각 “lvalue reference”, “lvalue constant reference”, “rvalue reference” 메시지가 출력되었다. 하지만 래퍼 함수인 templateFunc 함수를 거친 경우에는 세 가지 경우 모두 void func(Base& param)함수가 호출되어 “lvalue reference” 메시지가 출력되었다.
templateFunc(cb);의 결과가 “lvalue reference”로 나오는 이유(“lvalue constant reference”가 아닌 이유)는 C++ 컴파일러가template타입T를 추론할 때,T가 레퍼런스(reference) 타입이 아니라면const가 없는 것으로 간주하기 때문이다.
즉, 아래의 코드에서 T가 전부 class Base로 추론되어 func(Base& param) 함수만 호출되었다.
template <typename T>
void templateFunc(T param) {
func(param);
}
templateFunc의 매개변수 T를 아래와 같이 레퍼런스 형태로 바꾸면, non-const 참조는 lvalue에만 바인딩 할 수 있기 때문에 templateFunc(Base()); 문장에서 에러가 발생한다.
template <typename T>
void templateFunc(T& param) {
func(param);
}
그래서 templateFunc(T& param)와 templateFunc(const T& param) 2개의 래퍼 함수를 각각 생성하는 방법이 있다.
template <typename T>
void templateFunc(T& param) {
func(param);
}
template <typename T>
void templateFunc(const T& param) {
func(param);
}
=========Func=========
lvalue reference
lvalue constant reference
rvalue reference
=====TemplateFunc=====
lvalue reference
lvalue constant reference
lvalue constant reference
상수 레퍼런스만 받게 된다면, 상수가 아닌 레퍼런스도 상수 레퍼런스로 인식되므로 templateFunc(Base());의 호출 결과가 “lvalue constant reference”가 되었다.
그렇다면 rvalue reference를 받는 래퍼 함수(templateFunc(T&& param))를 하나 더 추가하여 func(Base&& param)가 호출되도록 할 수 있을 것이라는 생각이 들 수 있으나, 아래의 templateFunc 함수 내부에 진입했을 때 매개변수 param는 항상 lvalue이다.
template <typename T>
void templateFunc(T& param) {
func(param);
}
template <typename T>
void templateFunc(const T& param) {
func(param);
}
template <typename T>
void templateFunc(T&& param) {
func(param);
}
=========Func=========
lvalue reference
lvalue constant reference
rvalue reference
=====TemplateFunc=====
lvalue reference
lvalue constant reference
lvalue reference
그래서 이 상태로 실행을 해도 templateFunc(T&& param)를 호출한 결과에서는 “rvalue reference” 메시지를 볼 수가 없다. 래퍼 함수의 매개변수는 항상 lvalue이므로 lvalue reference를 받는 func(Base& param) 함수가 호출되기 때문이다.
그리고 만약에 템플릿 인자가 하나가 아니라면, 호출될 수 있는 상황을 고려하여 모든 조합의 템플릿 함수를 정의해야 하는 불편함이 있다.
하지만 C++11부터는 universal reference 개념을 통해 이 문제를 해결할 수 있다.
Universal reference
템플릿 인자 rvalue reference(T&&)는 그 표현 자체로는 rvalue reference를 의미하지만 때로는 rvalue 또는 lvalue reference로 해석될 수 있다. 문법상으로는 rvalue reference이지만, 실제 의미는 rvalue reference가 아닐 수 있다는 것이다. 이렇게 템플릿 인자 T에 대해서 우측값 레퍼런스를 받는 형태는 특이한 유연성을 갖으며 이러한 reference를 가리켜 universal reference라고 한다.
즉, 이전까지는 아래처럼 3가지 형태의 래퍼 함수를 모두 정의하였지만
template <typename T>
void templateFunc(T& param) {
func(param);
}
template <typename T>
void templateFunc(const T& param) {
func(param);
}
template <typename T>
void templateFunc(T&& param) {
func(param);
}
universal reference 개념을 적용한다면 위의 코드를 아래와 같이 하나의 형태로만 사용할 수 있고, 실행 결과는 다음과 같다.
template <typename T>
void templateFunc(T&& param) {
func(param);
}
=========Func=========
lvalue reference
lvalue constant reference
rvalue reference
=====TemplateFunc=====
lvalue reference
lvalue constant reference
lvalue reference
templateFunc(T&& param)의 매개변수 타입은 rvalue reference(T&&)이지만, lvalue reference와 lvalue constant reference를 잘 구분하였다.
여기서 중요한 것은 universal reference는 우측값만 받는 레퍼런스와는 다르다는 것이다.
#include <iostream>
class Base {
public:
Base() = default;
};
void func(Base&& param) { // 우측값만 받는 레퍼런스
std::cout << "rvalue reference" << std::endl;
}
int main() {
func(Base()); // valid
Base b;
func(b); // error : lvalue를 rvalue 참조에 바인딩할 수 없음
return 0;
}
Base&& param는 우측값만을 인자로 받을 수 있기 때문에, lvalue 형태로 호출할 수 없다.
Reference collapsing rule
아래 코드와 같이 템플릿에 사용된 우측값 레퍼런스(universal reference)는 우측값 뿐만 아니라 좌측값도 받아 낼 수 있다.
template <typename T>
void templateFunc(T&& param) {
func(param);
}
C++11에서는 reference collapsing rule에 따라 T의 타입을 추론하여 universal reference가 가능하게 한다.
#include <iostream>
class Base {
public:
Base() = default;
};
typedef Base& LR; // Lvalue Reference
typedef Base&& RR; // Rvalue Reference
int main() {
Base b;
LR ref1 = b;
RR ref2 = Base();
LR& ref3 = b; // Base& & -> Base&
LR&& ref4 = b; // Base& && -> Base&
RR& ref5 = b; // Base&& & -> Base&
RR&& ref6 = Base(); // Base&& && -> Base&&
// 아래와 같이 사용자가 명시적으로 reference collapsing를 일으키는 것은 불가능하다.
// reference collapsing는 컴파일러가 타입을 추론할 때 사용한다.
Base& & r7 = b;
Base& && r7 = b;
Base&& & r7 = b;
Base&&&& r7 = Base();
return 0;
}
lvalue reference와 rvalue reference 두 개의 레퍼런스 타입에 의해 4가지의 레퍼런스의 레퍼런스 조합이 존재할 수 있다. 편의상으로 L과 R로 정의한다.
- L : lvalue reference
- R : rvalue reference
| reference | reference | result |
|---|---|---|
| & | & | L + L |
| & | && | L + R |
| && | & | R + L |
| && | && | R + R |
두 가지의 reference collapsing 규칙이 다음과 같이 존재한다.
-
&& && (R + R)인 경우에만 &&
- 그 외 (L이 하나라도 포함된) 조합은 &
- 즉, 각 reference를
& = 1,&& = 0으로 생각하고 OR 연산한 결과와 같다.
reference collapsing rule을 적용한 결과는 다음과 같다.
| reference | reference | result | |
|---|---|---|---|
| & | & | L + L | & |
| & | && | L + R | & |
| && | & | R + L | & |
| && | && | R + R | && |
Perfect forwarding
위에서 살펴보았던 universal reference를 통해서 다음과 같은 템플릿 하나로도 실행을 할 수 있었지만
template <typename T>
void templateFunc(T&& param) {
func(param);
}
templateFunc(Base());의 호출 결과로 “lvalue reference”가 출력되었다.
templateFunc(T&& param)의 함수로 진입했을 때 매개변수 b는 항상 lvalue이므로 lvalue reference를 받는 func(Base& param) 함수가 호출되기 때문이다.
우리가 의도한 대로 인자를 전달하고 “rvalue reference” 메시지를 출력하기 위해서 std::forward<T>(param)를 사용할 수 있다.
이 경우에 templateFunc(Base()); 호출 결과가 “lvalue reference”로 출력되지 않는다.
#include <iostream>
template <typename T>
void templateFunc(T&& param) {
func(std::forward<T>(param));
}
class Base {
public:
Base() = default;
};
void func(Base& param) {
std::cout << "lvalue reference" << std::endl;
}
void func(const Base& param) {
std::cout << "lvalue constant reference" << std::endl;
}
void func(Base&& param) {
std::cout << "rvalue reference" << std::endl;
}
int main() {
Base b;
const Base cb;
std::cout << "=========Func=========" << std::endl;
func(b);
func(cb);
func(Base());
std::cout << "=====TemplateFunc=====" << std::endl;
templateFunc(b);
templateFunc(cb);
templateFunc(Base());
return 0;
}
=========Func=========
lvalue reference
lvalue constant reference
rvalue reference
=====TemplateFunc=====
lvalue reference
lvalue constant reference
rvalue reference
실행을 해보면 의도대로 잘 작동함을 확인할 수 있다.
templateFunc(b); // T: Base&, param: Base&
templateFunc(cb); // T: const Base&, param: const Base&
위의 두 가지 함수 호출은 reference collapsing rule에 의하여 templateFunc(T&& param)에서 T가 각각 Base&, const Base&로 추론되고,
templateFunc(Base()); // T: Base, param: Base&&
rvalue 형태로 호출하는 경우에는 T가 Base로 추론된다.
즉, 이전에 forward 함수를 사용하지 않았을 때에는
template <typename T>
void templateFunc(T&& param) {
func(param);
}
param이 Base&& 타입으로 추론되어도 lvalue이기 때문에 func(Base& param) 함수가 호출되었다.
이것을 방지하기 위해 Base&& 타입으로 추론된 경우에는
template <typename T>
void templateFunc(T&& param) {
func(std::forward<T>(param));
}
우측값으로 변환하기 위하여 forward 함수를 사용하여 func(Base&& param) 함수가 호출되도록 할 수 있다.
forward 함수는 다음과 같이 <utility>에 정의되어 있다. 두 가지 형태의 forward 함수가 존재하지만, 여기서는 일반적으로 가장 많이 사용되는 형태만 확인한다.
template< class T >
T&& forward( typename std::remove_reference<T>::type& t ) noexcept {
return static_cast<T&&>(t)
}
std::remove_reference는 타입의 레퍼런스를 제거하는 템플릿 메타 함수이다.
여기서 forward 함수 매개변수 타입이 &인 이유는 다음과 같다.
첫 번째는 <class T>에서 T가 Base&인 경우이다.
Base&&& forward( typename std::remove_reference<Base&>::type& t ) noexcept {
return static_cast<Base&&&>(t)
}
이것은 reference collapsing rule에 의해 다음과 같이 변한다.
Base& forward( typename std::remove_reference<Base&>::type& t ) noexcept {
return static_cast<Base&>(t)
}
즉, templateFunc(T&& param)에서 param 타입이 lvalue인 경우에는 그대로 lvalue 형태를 반환한다.
두 번째는 <class T>에서 T가 Base인 경우이다.
Base&& forward( typename std::remove_reference<Base>::type& t ) noexcept {
return static_cast<Base&&>(t)
}
이 경우 T 위치에 Base가 그대로 들어가게 되며, rvalue 형태로 반환된다.
이 과정을 순서대로 나타내면 아래와 같다.
templateFunc(Base()); // T: Base, param: Base&&
template <typename T>
void templateFunc(T&& param) { // T: Base, param: Base&&
func(std::forward<T>(param));
}
Base&& forward( typename std::remove_reference<Base>::type& t ) noexcept {
return static_cast<Base&&>(t) // return rvalue
}
void func(Base&& param) {
std::cout << "rvalue reference" << std::endl;
}
결과적으로 템플릿을 통한 래퍼 함수에서도 universal reference 개념과 std::forward 함수를 사용하여 인자가 잘 전달되도록 만들어 호출되는 함수가 동일해지도록 만들 수 있다.