템플릿으로 선언된 오버로딩 후보들을 살펴볼 때, 템플릿 인자들의 타입들을 유추한 후에 이로 치환하는 과정에서 말도 안되는 코드를 생산할 때가 있다.
int negate(int i) { return -i; }
template <typename T>
typename T::value_type negate(const T& t)
{
return -T(t);
}
negate(42) 를 생각해보면, 컴파일러는 아마 첫 번째 오버로딩 후보를 택해서 -42를 리턴한다. 하지만 컴파일러가 아래의 후보를 확인하는 과정에서 인자 T를 int로 추론하게 되는데, 이 과정에서 괴물 코드가 생성된다.
int ::value_type negate(const int& t)
{
/* ... */
}
int 에는 value_type 이라는 멤버가 없기 때문에 잘못된 코드이다. 이 경우 컴파일러가 컴파일 오류 메세지를 내뱉을까?
C++ 표준에서는 이와 같은 상황에 대해 컴파일러가 어떻게 동작할지 규칙을 정해놓았다.
템플릿 인자 치환에 실패할 경우 (위 같은 경우) 컴파일러는 이 오류를 무시하고, 그냥 오버로딩 후보에서 제외하면 된다.
만일 템플릿 인자 치환이 올바르지 않는 타입이나 구문을 생성한다면 타입 유추는 실패합니다. 올바르지 않는 타입이나 구문이라 하면, 치환된 인자로 썼을 때 문법상 틀린 것을 의미 합니다. 이 때, 함수의 즉각적인 맥락(immediate context)의 타입이나 구문만이 고려되고, 여기에서 발생한 오류 만이 타입 유추를 실패시킬 수 있습니다. 그 이후에, 올바르지 않다고 여겨지는 여러가지 상황들을 확인하면서 (예컨대 클래스가 아닌 타입이나, void 의 레퍼런스를 생성한다든지 등등) 이를 오버로딩 후보 목록에서 제외시킵니다.
C++ 에선 흔히 이를 치환 실패는 오류가 아니다 - Substitution Failure Is Not An Error 혹은 줄여서 SFINAE 라고 한다.
여기서 주의깊게 봐야할 부분은 즉각적인 맥락의 타입이나 구문만이 고려된다는 것이다.
함수의 즉각적인 맥락이 무엇을 지칭하는 것인가?
template <typename T>
void negate(const T& t)
{
typename T::value_type n = -t();
}
negate('c) 의 경우 T는 char가 되고 앞서 말했던 내용대로라면 컴파일 오류를 내지않고 오버로딩 후보에서 제외될 것이다. 하지만 이는 컴파일 오류가 발생한다.
이 경우는 함수 타입과 템플릿 타입 인자의 즉각적인 맥락 바깥에 있기 때문이다. (함수 내부에서 T::value_type)
따라서 SFINAE를 적용시키고 싶다면 함수의 선언부에 올바르지 않은 타입을 넣어서 타입 치환 오류를 발생시켜야 한다.
이를 통해 컴파일러는 해당 함수를 오버로딩 후보군에서 제외시킬 것이고 쓸데없는 컴파일 오류를 발생시키기 않게 된다.
SFINAE 를 잘 활용하는 툴들 중 가장 널리 쓰이는 것이 바로 enable_if 이다.
template <bool, typename T = void>
struct enable_if {};
template <typename T>
struct enable_if<true, T> {
typedef T type;
};
template <class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
{
std::cout << "do_stuff integral\n";
// 정수 타입들을 받는 함수 (int, char, unsigned, etc.)
}
template <class T, typename std::enable_if<std::is_class<T>::value, T>::type* = nullptr>
void do_stuff(T& t)
{
// 일반적인 클래스들을 받음
}
여기서 do_stuff(int 변수) 와 같이 함수를 호출하면 컴파일러는 첫 번째 함수를 고른다. 왜냐하면 is_integral 가 참이기 때문이다.
두 번째 오버로딩은 후보군에서 제외되는데 왜냐하면 is_class 가 false 이므로 type 이 정의되지 않는 일반적인 형태의 struct enable_if 가 선택되서 치환 오류가 발생하기 때문이다.
(false 로 골라진 struct enable_if 에는 type을 정의하지 않았기에 ::type* = nullptr> 이부분은 오류다.)
직접 경험한 enable_if 실제 사례
씹어먹는 C ++ 토막글 3 - SFINAE 와 enable_if
[C++] SFINAE 와 enable_if의 사용법