SFINAE
"替换失败并非错误"
该规则适用于函数模板的重载解析过程:当为模板参数
替换
显式指定或
推导类型
失败时,该特化会从
重载集
中剔除,而不会导致编译错误。
此特性用于模板元编程。
目录 |
说明
函数模板参数会被替换(被模板实参替代)两次:
- 显式指定的模板参数在模板实参推导之前进行替换
- 推导出的参数以及从默认值获取的参数在模板实参推导之后进行替换
替换发生在
- 函数类型中使用的所有类型(包括返回类型和所有参数的类型)
- 模板参数声明中使用的所有类型
- 部分特化的模板实参列表中使用的所有类型
|
(since C++11) |
|
(since C++20) |
当使用替换参数书写时,若上述类型或表达式将形成错误格式(需要诊断信息),则出现 替换失败 的情况。
只有函数类型或其模板参数类型 或其 显式说明符 (since C++20) 的 直接上下文 中的类型和表达式失败才属于SFINAE错误。如果被替换类型/表达式的求值引发副作用(例如实例化某些模板特化、生成隐式定义的成员函数等),这些副作用中的错误将被视为硬错误。 lambda表达式 不被视为直接上下文的一部分。 (since C++20)
|
本小节内容不完整
原因:缺少相关性的简要示例 |
替换按词法顺序进行,并在遇到失败时停止。
|
如果存在多个具有不同词法顺序的声明(例如,使用尾随返回类型声明的函数模板,在参数之后进行替换,而使用普通返回类型重新声明,该返回类型将在参数之前进行替换),并且这将导致模板实例化以不同顺序发生或根本不发生,则程序是非良构的;无需诊断。 |
(since C++11) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // 若T无成员类型则SFINAE失败 class V = typename B<T>::type> // 若B无成员类型则产生硬错误 // (根据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 数组、引用数组、函数数组、负大小数组、非整数大小数组或零大小数组:
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 成员不是非类型成员 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 允许创建替换失败,以便根据编译时评估的条件启用或禁用特定重载。 此外,如果无法使用适当的编译器扩展,许多 类型特征 必须通过 SFINAE 实现。 |
(since C++11) |
|
标准库组件 std::void_t 是另一个工具元函数,它简化了部分特化 SFINAE 的应用。 |
(since C++17) |
替代方案
在适用的情况下,通常优先使用
标签分发
、
if constexpr
(C++17 起)
和
概念
(C++20 起)
,而非 SFINAE。
|
|
(since C++11) |
示例
一种常见惯用法是在返回类型上使用表达式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++标准。
| 缺陷报告 | 适用标准 | 发布时行为 | 正确行为 |
|---|---|---|---|
| CWG 295 | C++98 |
创建cv限定函数类型
可能导致替换失败 |
改为不失败,
丢弃cv限定符 |
| CWG 1227 | C++98 | 替换顺序未作规定 | 与词法顺序相同 |
| CWG 2054 | C++98 | 偏特化中的替换未正确定义 | 已明确定义 |
| CWG 2322 | C++11 |
不同词法顺序的声明会导致模板
实例化顺序不同或完全不实例化 |
此类情况属于病式,
不要求诊断 |