[EMC++] Item 30. perfect forwarding이 실패하는 경우들을 잘 알아둬라
Item 30. perfect forwarding이 실패하는 경우들을 잘 알아둬라
perfect forwarding
perfect forwarding은 함수가 자신의 인자들을 그대로 다른 함수에 전달하는 것을 말한다.
perfect forwarding은 넘겨주는 객체들의 lvalue, rvalue 여부와 cv 지정자(const
, volatile
) 여부까지 그대로 전달해줘야 한다.
perfect forwarding이 가능한 경우는 템플릿 함수의 매개변수가 universal reference인 경우밖에 없다.
// 인자가 하나인 경우
template <typename T>
void fwd(T&& param) {
f(std::forward<T>(param);
}
// 인자가 여러 개인 경우
template <typename... Ts>
void fwd(Ts&&... param) {
f(std::forward<Ts>(param)...);
}
이는 perfect forwarding 함수의 공통적인 형태로 표준 컨테이너의 emplace류 함수들과 make 함수(std::make_unique
, std::make_shared
)의 구현에 사용된다.
perfect forwarding이 실패하는 경우
perfect forwarding이 실패하는 경우는 다음과 같다.
brace initializer를 인자로 사용하는 경우
brace initializer를 인자로 사용하는 경우 perfect forwarding에 실패한다.
void f(const std::vector<int>& v);
f({ 1, 2, 3 }); // { 1, 2, 3 }이 암묵적으로 std::vector<int>로 변환
fwd({ 1, 2, 3 }); // 컴파일 에러, perfect forwarding 실패
위와 같은 상황에서 f
함수를 직접 호출하게 되면 컴파일러는 brace initializer와 매개변수 타입을 비교해서 적절하게 암묵적 변환을 수행한다.
하지만 perfect forwarding 템플릿 함수인 fwd
함수는 컴파일러가 brace initializer의 타입을 추론하고 추론된 타입들을 바탕으로 f
함수의 매개변수와 비교해야 한다.
이때 두 조건 중 하나라도 만족하면 perfect forwarding이 실패한다.
- 컴파일러가 타입을 추론할 수 없는 경우
- 컴파일러가 잘못된 타입을 추론하는 경우
템플릿은 std::initializer_list
타입을 추론할 수 없으므로 perfect forwarding에 실패한다.
perfect forwarding에 성공하고 싶으면 auto
를 통해 먼저 std::initializer_list
타입을 추론한 후 인자로 넘겨줘야 한다.
auto il = { 1, 2, 3 };
fwd(il); // perfect forwarding 성공
0이나 NULL을 null pointer 인자로 사용하는 경우
Item 8에서 이야기했듯이 0
이나 NULL
을 null pointer로써 템플릿 인자로 넘겨주면 컴파일러가 이를 포인터 타입이 아닌 정수 타입으로 추론하여 perfect forwarding에 실패한다.
선언만 된 static const 정수 멤버를 인자로 사용하는 경우
일반적으로 static const
정수 멤버는 선언만 해도 문제없이 사용할 수 있다.
class Widget {
public:
static constexpr std::size_t MinVals = 28;
};
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // MinVals를 선언만 해도 사용 가능
이는 컴파일러가 상수 전파(const propagation)을 적용해서 마치 매크로처럼 MinVals
를 사용하는 곳마다 28이라는 값으로 치환하기 때문이다.
따라서 static const
정수 멤버에 대한 메모리를 따로 마련할 필요가 없다.
하지만 perfect forwarding 템플릿 함수의 인자로 static const
정수 멤버를 사용하면 문제가 발생한다.
void f(std::size_t val);
f(Widget::MinVals); // f(28)로 처리됨
fwd(Widget::MinVals); // 컴파일 에러, 링크에 실패함
fwd
함수는 매개 변수 타입이 universal reference이므로 해당 인자의 주소 값을 참조한다.
하지만 static const
정수 멤버는 선언만 존재하기 때문에 링킹 과정에서 에러가 발생한다. 해당 static const
정수 멤버의 정의를 찾지 못했기 때문이다.
따라서 perfect forwarding에 사용하고 싶으면 해당 파일에 직접 정의 해주면 된다.
constexpr std::size_t MinVals = 28;
overloading 함수와 template 함수를 인자로 사용하는 경우
overloading 함수나 template 함수를 인자로 사용하는 경우 perfect forwarding에 실패한다.
함수를 인자로 받아서 이를 실행하는 함수 f
가 있다고 가정해보자.
// 함수를 받아서 실행하는 함수
void f(int (*pf)(int));
// 위와 같은 의미인 비 포인터 구문
void f(int pf(int));
그 다음 overloading 된 processVal
함수를 인자로 사용하면 정상적으로 동작한다.
int processVal(int value);
int processVal(int value, int priority);
f(processVal); // 가능
이는 컴파일러가 f
함수의 매개변수 타입과 일치하는 overloading 함수를 선택해서 넘겨주기 때문이다.
이런 일이 가능한 이유는 컴파일러가 f
함수의 선언을 보고 타입을 알 수 있기 때문이다.
하지만 perfect forwarding 템플릿 함수의 인자로 overloading 함수를 넣으면 실패한다.
fwd(processVal); // 컴파일 에러, 어떤 overloading 함수를 사용할 지 모름
컴파일러가 fwd
함수의 선언을 보고 타입에 대한 정보를 얻을 수 없기 때문이다.
이는 overloading 함수 대신 template 함수를 사용해도 마찬가지이다.
template 함수는 하나의 함수를 나타내는 것이 아니라 여러 함수를 대표한다.
template <typename T>
T workOnVal(T param) {}
fwd(workOnVal); // 컴파일 에러, 어떤 템플릿 인스턴스를 사용할 지 모름
perfect forwarding에 성공하기 위해서는 전달하고자 하는 overloading 함수나 template 함수를 명시적으로 지정하면 된다.
using ProcessFuncType = int (*)(int);
ProcessFuncType processValPtr = processVal;
fwd(processValPtr); // 성공, perfect forwarding 가능
fwd(static_cast<ProcessFuncType>(workOnVal)); // 성공, perfect forwarding 가능
비트필드(bitfield)를 인자로 사용하는 경우
비트필드(bitfield)를 인자로 사용하는 경우 perfect forwarding에 실패한다.
bitfield는 구조체(struct)에서 정수형 데이터를 비트 단위로 나누어서 사용할 수 있는 기능을 말한다.
struct 구조체 {
unsigned 정수형 멤버명1 : 비트수;
unsigned 정수형 멤버명2 : 비트수;
};
IPv4 헤더를 나타내는 구조체가 있다고 가정해보자.
struct IPv4Header {
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
};
void f(std::size_t);
IPv4Header h;
f(h.totalLength); // 가능
이처럼 IPv4Header
객체의 totalLength
필드를 호출하는 코드는 문제없이 동작한다.
하지만 perfect forwarding 템플릿 함수의 인자로 bitfield를 넣으면 실패한다.
fwd(h.totalLength); // 컴파일 에러, non-const bitfield는 불가능
그 이유는 템플릿 매개변수가 universal reference이고 h.totalLength
가 non-const bitfield이기 때문이다.
C++ 표준에서는 non-const reference는 bitfield를 참조할 수 없다고 명시되어 있다.
그 이유는 컴파일러가 주소값을 참조할 때 bit 단위로 참조하지 않기 때문이다.
이를 해결하기 위해서 값으로 전달하는 방법과 const reference로 참조하는 방법이 있다.
여기서 특이한 점은 const reference를 사용해도 bitfield 값을 복사하여 사용하는 점이다.
표준에 따르면 const reference로 bitfield를 참조하는 경우 bitfield 원본이 아닌 bitfield 값이 복사된 객체를 참조한다.
즉, 무슨 방법을 써도 bitfield 값을 복사하여 사용한다.
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length); // 성공, perfect forwarding 가능
이렇게 bitfield 값의 복사본을 넘기면 문제없이 perfect forwarding이 가능하다.