SFINAE
"치환 실패는 오류가 아니다"
이 규칙은 함수 템플릿의 오버로드 해결 중에 적용됩니다:
템플릿 매개변수에 대해 명시적으로 지정되거나
추론된 타입
을
치환
하는 데 실패할 경우, 해당 특수화는 컴파일 오류를 발생시키는 대신
오버로드 집합
에서 제외됩니다.
이 기능은 템플릿 메타프로그래밍에서 사용됩니다.
목차 |
설명
함수 템플릿 매개변수는 두 번 치환됩니다(템플릿 인수로 대체됨):
- 명시적으로 지정된 템플릿 인수는 템플릿 인수 추론 전에 치환됩니다
- 추론된 인수와 기본값에서 얻은 인수는 템플릿 인수 추론 후에 치환됩니다
치환은 다음에서 발생합니다
- 함수 타입에 사용된 모든 타입 (반환 타입 및 모든 매개변수 타입 포함)
- 템플릿 매개변수 선언에 사용된 모든 타입
- 부분 특수화의 템플릿 인수 목록에 사용된 모든 타입
|
(since C++11) |
|
(C++20부터) |
치환 실패 는 위의 타입이나 표현식이 치환된 인자를 사용하여 작성되었을 때 형식에 맞지 않는(필수 진단과 함께) 상황을 의미합니다.
함수 타입 또는 그 템플릿 매개변수 타입들의 직접적인 문맥(immediate context) 에 있는 타입과 표현식에서의 실패만이 SFINAE 오류입니다. 치환된 타입/표현식의 평가가 일부 템플릿 특수화의 인스턴스화, 암시적으로 정의된 멤버 함수의 생성 등과 같은 부수 효과를 유발하는 경우, 해당 부수 효과에서의 오류는 하드 오류로 처리됩니다. explicit specifier (C++20부터) (since C++20) lambda expression 은 직접적인 문맥의 일부로 간주되지 않습니다. (since C++20)
|
이 섹션은 불완전합니다
이유: 중요성을 보여주는 미니 예시 |
치환은 어휘 순서로 진행되며 실패가 발생하면 중단됩니다.
|
서로 다른 어휘 순서를 가진 여러 선언이 존재하는 경우(예: 매개변수 뒤에서 치환되는 후행 반환 타입으로 선언된 함수 템플릿과 매개변수 앞에서 치환되는 일반 반환 타입으로 재선언된 경우), 그리고 이것이 템플릿 인스턴스화가 다른 순서로 발생하거나 전혀 발생하지 않도록 하는 경우, 프로그램은 형식 오류이며 진단은 필요하지 않습니다. |
(C++11부터) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // T에 멤버 type이 없을 경우 SFINAE 실패 class V = typename B<T>::type> // B에 멤버 type이 없을 경우 하드 에러 // (CWG 1227에 의해 U의 기본 템플릿 인자로의 // 치환이 먼저 실패하므로 발생하지 않음이 보장됨) void foo (int); template<class T> typename T::type h(typename B<T>::type); template<class T> auto h(typename B<T>::type) -> typename T::type; // 재선언 template<class T> void h(...) {} using R = decltype(h<int>(0)); // 형식 오류, 진단 불필요
타입 SFINAE
다음 유형 오류는 SFINAE 오류입니다:
|
(since C++11) |
- void 배열, 참조 배열, 함수 배열, 음수 크기 배열, 정수가 아닌 크기의 배열, 크기가 0인 배열을 생성하려는 시도:
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // 이 오버로드는 I가 짝수일 때 선택됩니다 } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // 이 오버로드는 I가 홀수일 때 선택됩니다 }
-
범위 지정 연산자 왼쪽에 클래스나 열거형이 아닌 타입을 사용하려는 시도:
::
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // 두 번째 오버로드를 사용함
- 타입의 멤버를 사용하려고 시도하는 경우
-
- 해당 타입이 지정된 멤버를 포함하지 않음
- 타입이 필요한 위치에서 지정된 멤버가 타입이 아님
- 템플릿이 필요한 위치에서 지정된 멤버가 템플릿이 아님
- 논-타입이 필요한 위치에서 지정된 멤버가 논-타입이 아님
template<int I> struct X {}; template<template<class T> class> struct Z {}; template<class T> void f(typename T::Y*) {} template<class T> void g(X<T::N>*) {} template<class T> void h(Z<T::template TT>*) {} struct A {}; struct B { int Y; }; struct C { typedef int N; }; struct D { typedef int TT; }; struct B1 { typedef int Y; }; struct C1 { static const int N = 0; }; struct D1 { template<typename T> struct TT {}; }; int main() { // 다음 각 경우에서 추론이 실패합니다: f<A>(0); // A는 Y 멤버를 포함하지 않음 f<B>(0); // B의 Y 멤버는 타입이 아님 g<C>(0); // C의 N 멤버는 비타입(non-type)이 아님 h<D>(0); // D의 TT 멤버는 템플릿이 아님 // 다음 각 경우에서 추론이 성공합니다: f<B1>(0); g<C1>(0); h<D1>(0); } // 할일: 단순 실패뿐만 아니라 오버로드 해결도 보여줘야 함
- 참조에 대한 포인터 생성 시도
- void에 대한 참조 생성 시도
- T가 클래스 타입이 아닐 때 T의 멤버에 대한 포인터 생성 시도:
template<typename T> class is_class { typedef char yes[1]; typedef char no[2]; template<typename C> static yes& test(int C::*); // C가 클래스 타입일 때 선택됨 template<typename C> static no& test(...); // 그렇지 않을 때 선택됨 public: static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes); };
- 상수 템플릿 매개변수에 유효하지 않은 타입을 지정하려는 시도:
template<class T, T> struct S {}; template<class T> int f(S<T, T()>*); struct X {}; int i0 = f<X>(0); // 할 일: 단순 실패가 아닌 오버로드 해결을 보여줘야 함
- 유효하지 않은 변환을 수행하려고 시도 중입니다
-
- 템플릿 인자 표현식 내에서
- 함수 선언에 사용된 표현식에서:
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // 1을 int*로 변환할 수 없음 // 할일: 단순 실패가 아닌 오버로드 해결을 보여줘야 함
- void 타입의 매개변수를 가진 함수 타입을 생성하려는 시도
- 배열 타입이나 함수 타입을 반환하는 함수 타입을 생성하려는 시도
표현식 SFINAE
|
C++11 이전에는 타입(예: 배열 경계)에 사용되는 상수 표현식만 SFINAE(치환 실패는 오류가 아님)로 처리되어야 했습니다(심각한 오류가 아닌 것으로). |
(until C++11) |
|
다음 표현식 오류들은 SFINAE 오류입니다
struct X {}; struct Y { Y(X){} }; // X is convertible to Y template<class T> auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1 X f(Y, Y); // overload #2 X x1, x2; X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed) // only #2 is in the overload set, and is called |
(since C++11) |
부분 특수화에서의 SFINAE
추론과 치환은 클래스 또는 변수 (since C++14) 템플릿의 특수화가 부분 특수화 또는 기본 템플릿에 의해 생성되는지 여부를 결정하는 동안에도 발생합니다. 이러한 결정 과정에서 치환 실패는 심각한 오류로 처리되지 않으며, 함수 템플릿을 포함한 오버로드 해결에서와 마찬가지로 해당 부분 특수화 선언이 무시됩니다.
// 기본 템플릿은 참조 불가능 타입을 처리합니다: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // 특수화는 참조 가능 타입을 인식합니다: template<class T> struct reference_traits<T, std::void_t<T&>> { using add_lref = T&; using add_rref = T&&; }; template<class T> using add_lvalue_reference_t = typename reference_traits<T>::add_lref; template<class T> using add_rvalue_reference_t = typename reference_traits<T>::add_rref;
라이브러리 지원
|
표준 라이브러리 컴포넌트 std::enable_if 는 컴파일 시간에 평가되는 조건에 기반하여 특정 오버로드를 활성화하거나 비활성화하기 위해 치환 실패를 생성할 수 있도록 합니다. 또한, 적절한 컴파일러 확장 기능을 사용할 수 없는 경우 많은 type traits 는 SFINAE를 사용하여 구현되어야 합니다. |
(since C++11) |
|
표준 라이브러리 구성 요소 std::void_t 는 부분 특수화 SFINAE 응용을 단순화하는 또 다른 유틸리티 메타함수입니다. |
(since C++17) |
대안
해당되는 경우,
태그 디스패치
,
if constexpr
(C++17부터)
, 그리고
개념(concepts)
(C++20부터)
가 일반적으로 SFINAE 사용보다 선호됩니다.
|
|
(C++11부터) |
예제
일반적인 관용구는 반환 타입에 expression SFINAE를 사용하는 것으로, 이 표현식은 콤마 연산자를 사용하며, 왼쪽 부분식이 검사 대상입니다(사용자 정의 콤마 연산자가 선택되지 않도록 void로 캐스팅). 오른쪽 부분식은 함수가 반환해야 할 타입을 가집니다.
#include <iostream> // This overload is added to the set of overloads if C is // a class or reference-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)(c.*f)(), void()) { std::cout << "(1) Class/class reference overload called\n"; } // This overload is added to the set of overloads if C is a // pointer-to-class type and F is a pointer to member function of C template<class C, class F> auto test(C c, F f) -> decltype((void)((c->*f)()), void()) { std::cout << "(2) Pointer overload called\n"; } // This overload is always in the set of overloads: ellipsis // parameter has the lowest ranking for overload resolution void test(...) { std::cout << "(3) Catch-all overload called\n"; } int main() { struct X { void f() {} }; X x; X& rx = x; test(x, &X::f); // (1) test(rx, &X::f); // (1), creates a copy of x test(&x, &X::f); // (2) test(42, 1337); // (3) }
출력:
(1) Class/class reference overload called (1) Class/class reference overload called (2) Pointer overload called (3) Catch-all overload called
결함 보고서
다음의 동작 변경 결함 보고서들은 이전에 발표된 C++ 표준에 소급 적용되었습니다.
| DR | 적용 대상 | 게시된 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 295 | C++98 |
cv-한정 함수 타입 생성 시
치환 실패가 발생할 수 있음 |
실패하지 않도록 변경,
cv-한정 폐기 |
| CWG 1227 | C++98 | 치환 순서가 명시되지 않음 | 어휘적 순서와 동일하게 지정 |
| CWG 2054 | C++98 | 부분 특수화에서의 치환이 올바르게 명시되지 않음 | 명시됨 |
| CWG 2322 | C++11 |
서로 다른 어휘적 순서의 선언들이
템플릿 인스턴스화 순서를 다르게 하거나 발생하지 않도록 할 수 있음 |
해당 경우는 형식 오류,
진단 불필요 |