[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
함수를 사용하여 인자가 잘 전달되도록 만들어 호출되는 함수가 동일해지도록 만들 수 있다.