SFINAE(Substitution failure is not an error)
씹어먹는 C ++ 토막글 3 - SFINAE 와 enable_if
모두의 코드 씹어먹는 C ++ 토막글 3 - SFINAE 와 enable_if 작성일 : 2019-01-05 이 글은 19113 번 읽혔습니다. 이 글은 여기 에 실린 SFINAE and enable_if 글을 바탕으로 쓰였습니다. C++ 에서 템플릿과 함수의 오
modoocode.com
SFINAE는 치환실패는 오류가 아니다라는 말이다.
템플릿 매개변수에 자료형이나 값을 넣을 수 없어도 컴파일 오류가 발생하지 않고 해당 템플릿에 대해서는 코드 생성을 무시하는 현상. 템플릿 매개변수 입력에 제약을 걸어주고 함수 오버로딩 시 프로그래머가 함수 선택을 제어할 수 있게한다.
특정 형식에 대해서만 템플릿이 동작하도록 하고 싶다면 C++20에서 추가된 requres를 사용하는 것이 유용해 보인다.
SFINAE(Substitution Failure Is Not An Error)는 C++ 템플릿 메타프로그래밍에서 사용되는 중요한 개념 중 하나. SFINAE는 템플릿 인자로 주어진 타입에 대한 일부 연산 또는 함수를 지원하는 경우 해당 함수 또는 클래스 템플릿을 사용하고, 그렇지 않은 경우 컴파일 시간 오류를 발생시키지 않고 무시하는 원리.
1. 템플릿 추론: C++ 컴파일러는 템플릿 인자를 추론하고 함수 또는 클래스 템플릿을 생성합니다.
2. 함수 오버로딩 및 템플릿 선언: 다양한 버전의 함수 또는 클래스 템플릿을 만듭니다. 이 때, SFINAE를 사용하여 특정 조건을 충족하지 못하는 버전의 함수 또는 템플릿을 제거하려고 합니다.
3. 함수 호출 또는 인스턴스화: 코드에서 해당 함수를 호출하거나 클래스 템플릿을 인스턴스화합니다.
4. 후보 선택: 컴파일러는 호출 또는 인스턴스화 시에 어떤 버전의 함수 또는 템플릿을 사용할지 결정해야 합니다.
5. SFINAE 적용: 컴파일러는 후보 함수 또는 템플릿 중에서 템플릿 인자로 주어진 타입에서 사용할 수 없는 버전을 제거합니다. 이로 인해 해당 함수 또는 템플릿 버전은 후보에서 제외됩니다.
6. 최종 선택: 컴파일러가 SFINAE를 적용한 후에 남은 후보 중에서 호출할 함수 또는 템플릿을 선택하고 컴파일합니다.
SFINAE의 가장 흔한 사용 사례는 타입 특성 또는 타입 멤버 함수의 존재 여부를 기반으로 특수화된 동작을 제공하는 경우다. 이를 통해 런타임 오류가 아닌 컴파일 타임 오류를 방지하고 더 안정적인 코드를 작성할 수 있다.
아래는 씹어먹는 C++에서 발췌한 내용이다.
예제를 보실까요.
void foo(unsigned int i) { std::cout << "unsigned " << i << "\n"; }
template <typename T>
void foo(const T& t) {
std::cout << "template " << t << "\n";
}
여러분이 foo(42) 수행 시에 어떤 메세지가 호출될까요? 답은 바로 "template 42" 입니다. 왜냐하면 정수 리터럴의 경우 디폴트로 부호 있는 정수가 되기 때문에 (끝에 U 를 붙이지 않는 이상 부호 있는 정수가 됩니다), 컴파일러가 오버로딩 후보들을 살펴볼 때 첫 번째 버전은 타입 변환이 필요하지만 (unsigned 를 붙여야 되죠), 두 번째 버전은 타입 변환 없이 그냥 T 를 int 로 끼워넣으면 되기 때문에 결국 두 번째 후보가 채택됩니다.
컴파일러가 템플릿으로 선언된 오버로딩 후보들을 살펴볼 때, 템플릿 인자들의 타입들을 유추한 후에 이로 치환하는 과정에서 말도 안되는 코드를 생산할 때가 있습니다. 예를 들어서 아래의 예제를 살펴볼까요.
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++ 에서 템플릿을 사용하기 매우 어려울 것입니다. 실제로 C++ 표준에는 이와 같은 상황에 대해 컴파일러가 어떻게 동작해야할지 규칙을 정해놓았습니다.
템플릿 인자 치환에 실패할 경우 (위 같은 경우) 컴파일러는 이 오류를 무시하고, 그냥 오버로딩 후보에서 제외하면 된다 라고 명시되어 있습니다.
C++ 에선 흔히 이를 치환 실패는 오류가 아니다 - Substitution Failure Is Not An Error 혹은 줄여서 SFINAE 라고 합니다.
C++ 표준 문구를 정확히 읊어보자면 아래와 같습니다.
만일 템플릿 인자 치환이 올바르지 않는 타입이나 구문을 생성한다면 타입 유추는 실패합니다. 올바르지 않는 타입이나 구문이라 하면, 치환된 인자로 썼을 때 문법상 틀린 것을 의미 합니다. 이 때, 함수의 즉각적인 맥락(immediate context)의 타입이나 구문만이 고려되고, 여기에서 발생한 오류 만이 타입 유추를 실패시킬 수 있습니다. 그 이후에, 올바르지 않다고 여겨지는 여러가지 상황들을 확인하면서 (예컨대 클래스가 아닌 타입이나, void 의 레퍼런스를 생성한다든지 등등) 이를 오버로딩 후보 목록에서 제외시킵니다.
근데 여기서 함수의 즉각적인 맥락(immediate context)이 무엇을 지칭하는 것일까요?
사실 immediate context 를 딱히 한국말로 뭐라 옮길지 몰라서 이렇게 썼습니다.
아래와 같은 함수를 살펴보도록 합시다.
template <typename T>
void negate(const T& t) {
typename T::value_type n = -t();
}
만일 value_type 을 멤버로 가지지 않는 어떤 타입에 대해 위 템플릿의 타입 유추를 수행하였다고 해봅시다. 예를 들어 negate('c') 의 경우 T 는 char 가 되겠네요. 아무튼, 이를 컴파일 해보면, SFINAE 덕분에 컴파일 오류가 나타나지 않는 것이 아니라, 함수 내부의 T::value_type 때문에 컴파일 오류가 발생합니다.
왜 이 경우 SFINAE 가 적용되지 않았을까요? 왜냐하면 T::value_type 는 함수 타입과 템플릿 타입 인자의 즉각적인 맥락 바깥에 있기 때문입니다. 따라서 표준 규정에 따라 이는 SFINAE 의 적용 범위를 넘어섭니다.
따라서 만약에 우리가 특정한 타입들에게만 작동하는 템플릿을 작성하고 싶다면 (위 경우 value_type 이 멤버로 있는 타입들을 가리키는 것이겠지요?) 함수의 선언부에 올바르지 않은 타입을 넣어서 타입 치환 오류를 발생시켜야 합니다. 이를 통해 컴파일러는 해당 함수를 오버로딩 후보군에서 제외시킬 것이고 쓸데없는 컴파일 오류를 발생시키기 않게 됩니다.