* 정의
완벽 전달(Perfect forwarding)이란 한 함수가 처음 받았던 것과 동일한 인수들 그대로 다른 함수에 넘겨주는 것을 의미한다.
값 전달 방식(call by value)에서의 매개변수는 원래의 호출자가 넘겨준 인수의 복사본이기 때문에 불가능하고 포인터 매개변수는 호출자에게 포인터를 넘겨주도록 강제하는 것은 바람직하지 않기 때문에 불가능하다.
완벽 전달은 단순히 객체들을 전달하는 것 뿐만아니라 객체들의 주요 특징, 형식, 왼값 오른값, const, volatile 등도 전달하는 것을 의미한다. 이러한 것들을 수행할 수 있는 것은 보편 참조 매개변수뿐이다.
* 완벽 전달 실패
f에 완벽 전달을 수행하는 함수 fwd와 함수 f가 있다고 가정할 때 어떤 인수로 f를 호출했을 때와 fwd를 호출했을 때 결과가 다르다면 완벽 전달이 실패했다고 할 수 있다.
- 중괄호 초기치
중괄호 초기치 사용에서 완벽 전달이 실패한다.
void f(const std::vector<int>& input) {}
template<typename... Ts>
void fwd(Ts&&... params)
{
f(std::forward<Ts>(params)...);
}
void Test(void)
{
f({ 1,2,3 });
// 컴파일 에러
//fwd({ 1,2,3 });
}
f를 직접호출하면 컴파일러는 호출 지점에 함수에 전달된 인수들의 형식들과 f에 선언된 매개변수들의 형식들을 비교해서 호환 여부를 파악하고 필요에 따라 적절한 암묵적 변환을 수행해서 호출을 성사시킨다. 위의 코드에서는 {1,2,3}으로부터 임시 std::vector<int> 객체를 생성해서 f의 매개변수 input에 묶는다.
fwd를 통해 간접호출하게 되면 호출 지점에서 전달된 인수들과 f에 선언된 매개변수를 직접 비교할 수 없다. 컴파일러는 fwd에 전달되는 인수들의 형식을 연역하고 연역된 형식들과 f의 매개변수 선언들과 비교한다. 과정에서 다음 조건 중 하나라도 만족되면 완벽 전달이 실패한다.
- fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 연역하지 못한다.
- fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 잘못 연역한다. 잘못 연역하였을 때 그 형식으로 fwd의 인스턴스를 컴파일하지 못하거나 컴파일은 되지만 직접 호출할 때와 다르게 행동하는 결과를 가져온다.
여기서 문제는 std::initializer_list가 될 수 없는 형태로 선언된 함수 템플릿 매개변수에 중괄호 초기치를 넘겨준다는 것이다. 간단히 말하면 {1,2,3}의 형식을 컴파일러가 연역하는 것이 금지되어 fwd의 매개변수의 형식을 연역할 수 없는 뜻이다.
이를 방지하기 위해 auto 변수를 중괄호 초기치로 초기화하고 넘겨주면 된다.
auto input = { 1,2,3 };
fwd(input);
- 널 포인터를 의미하는 0 또는 NULL
0이나 NULL을 널 포인터로서 템플릿에 넘겨주려 하면 컴파일러가 이를 포인터 형식이 아닌 정수 형식으로 연역하기 때문에 문제가 생긴다. 해결책은 0또는 NULL 대신 nullptr을 사용하면 된다.
- 선언만 된 정수 static const 및 constexpr 자료 멤버
어떤 컴파일러에서는 static const, static constexpr 자료 멤버가 선언만 되어있고 정의가 되어있지 않을 때 해당 변수의 주소값을 취한다면 해당 변수에 대한 저장소가 따로 마련되어있지 않아 링크가 실패하는 경우가 있다.(정의가 없어서)
보편 참조 역시 참조를 사용하기 때문에 내부적으로 포인터와 비슷하게 동작하고 있다. 따라서 보편 참조 템플릿 함수 호출에 실패하는 경우가 있다. 해결 방법은 정의를 해주면 된다.
- 중복적재된 함수 이름과 템플릿 이름
함수 포인터를 매개 변수로 받는 함수로 중복적재된 함수를 완벽 전달하려고 할 때 문제가 생긴다.
int InputFunction(int a) { return a; }
int InputFunction(int a, int b) { return a; }
void f(int (*fp)(int)) {}
template<typename... Ts>
void fwd(Ts&&... params)
{
f(std::forward<Ts>(params)...);
}
void Test(void)
{
f(InputFunction);
// 형식이 없다 > 형식연역 불가능
//fwd(InputFunction);
}
그냥 직접 호출하는 것은 f의 선언에서 매개변수 형식과 일치하는 것을 찾으면 되기 때문에 적절한 중복적재 버전을 선택하여 넘겨줄 수 있다. 하지만 fwd 함수에 넘겨줄 때는 호출에 필요한 형식에 관한 정보가 없어서 어떤 중복적재 버전을 선택할지 결정하지 못한다.
이를 해결하기 위해서 전달하고자 하는 중복적재나 템플릿 인스턴스를 명시적으로 지정하면 된다. 다음과 같다.
using InputFunctionType = int (*) (int);
fwd(static_cast<InputFunctionType>(InputFunction));
- 비트필드
특정 빌트필드 직접적으로 지칭하는 방법은 없으며 임의의 비트들을 가리키는 포인터를 생성하는 방법은 없다. 따라서 비트필드를 인수로 받는 임의의 함수는 그 비트필드 값의 복사본을 받게 해주면 된다.
struct IPv4Header
{
std::uint32_t version : 4,
IHL : 4,
DSCP : 6,
ECN : 2,
totalLength : 16;
};
void f(std::uint16_t totalLength) {}
template<typename... Ts>
void fwd(Ts&&... params)
{
f(std::forward<Ts>(params)...);
}
void Test(void)
{
IPv4Header header;
// ...
// 불가능하다.
//fwd(header.totalLength);
auto length = static_cast<std::uint16_t>(header.totalLength);
fwd(length);
}