SFINAE
"替换失败不是错误"
在函数模板的重载解析过程中,当将显式指定的或推断出的类型替换为模板参数失败时,该特化将从重载集合中丢弃,而不是导致编译错误。
此特性在模板元编程中使用。
目录 |
[编辑] 说明
函数模板参数被替换(用模板参数替换)两次
- 显式指定的模板参数在模板参数推断之前被替换
- 推断出的参数和从默认值获得的参数在模板参数推断之后被替换
替换发生在
- 函数类型中使用的所有类型(包括返回值类型和所有参数的类型)
- 模板参数声明中使用的所有类型
- 部分特化的模板参数列表中使用的所有类型
|
(自 C++11 起) |
|
(自 C++20 起) |
替换失败是指任何情况下,如果使用替换后的参数编写上述类型或表达式,则该类型或表达式将是非法的(需要诊断)。
只有在函数类型或其模板参数类型或其 explicit 说明符(自 C++20 起) 的直接上下文 中的类型和表达式中的失败才属于 SFINAE 错误。如果替换后的类型/表达式的计算导致副作用,例如某个模板特化的实例化、隐式定义的成员函数的生成等,则这些副作用中的错误将被视为硬错误。 lambda 表达式 不被视为直接上下文的一部分。(自 C++20 起)
本节不完整 原因:此问题相关的迷你示例 |
替换按词法顺序进行,并在遇到失败时停止。
如果有多个声明具有不同的词法顺序(例如,一个带有尾部返回值类型的函数模板,将在参数之后进行替换,并且用普通返回值类型重新声明,该类型将在参数之前进行替换),并且这会导致模板实例化以不同的顺序发生或根本不发生,则程序是非法的;不需要诊断。 |
(自 C++11 起) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // SFINAE failure if T has no member type class V = typename B<T>::type> // hard error if B has no member type // (guaranteed to not occur via CWG 1227 because // substitution into the default template argument // of U would fail first) 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; // redeclaration template<class T> void h(...) {} using R = decltype(h<int>(0)); // ill-formed, no diagnostic required
[编辑] 类型 SFINAE
以下类型错误属于 SFINAE 错误
|
(自 C++11 起) |
- 尝试创建 void 数组、引用数组、函数数组、负大小数组、非整数大小数组或大小为零的数组
template<int I> void div(char(*)[I % 2 == 0] = nullptr) { // this overload is selected when I is even } template<int I> void div(char(*)[I % 2 == 1] = nullptr) { // this overload is selected when I is odd }
- 尝试在作用域解析运算符
::
的左侧使用类型,而该类型不是类或枚举
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // uses second overload
- 尝试使用类型的成员,其中
- 该类型不包含指定的成员
- 指定的成员不是类型,而需要类型
- 指定的成员不是模板,而需要模板
- 指定的成员不是非类型,而需要非类型
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() { // Deduction fails in each of these cases: f<A>(0); // A does not contain a member Y f<B>(0); // The Y member of B is not a type g<C>(0); // The N member of C is not a non-type h<D>(0); // The TT member of D is not a template // Deduction succeeds in each of these cases: f<B1>(0); g<C1>(0); h<D1>(0); } // todo: needs to demonstrate overload resolution, not just failure
- 尝试创建指向引用的指针
- 尝试创建指向 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::*); // selected if C is a class type template<typename C> static no& test(...); // selected otherwise 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); // todo: needs to demonstrate overload resolution, not just failure
- 尝试在以下位置执行无效转换
- 模板参数表达式中
- 函数声明中使用的表达式中
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // can’t conv 1 to int* // todo: needs to demonstrate overload resolution, not just failure
- 尝试创建类型为 void 的参数的函数类型
- 尝试创建返回数组类型或函数类型的函数类型
[编辑] 表达式 SFINAE
在 C++11 之前,只有在类型中使用的常量表达式(例如数组边界)才需要被视为 SFINAE(而不是硬错误)。 |
(直到 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 |
(自 C++11 起) |
[编辑] 部分特化中的 SFINAE
推断和替换也发生在确定类或变量(自 C++14 起) 模板的特化是通过某个 部分特化 还是主模板生成的。在这样的确定过程中,替换失败不被视为硬错误,而是使对应的部分特化声明被忽略,就像在涉及函数模板的重载解析中一样。
// primary template handles non-referenceable types: template<class T, class = void> struct reference_traits { using add_lref = T; using add_rref = T; }; // specialization recognizes referenceable types: 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 实现。 |
(自 C++11 起) |
标准库组件 std::void_t 是另一个实用元函数,简化了部分特化 SFINAE 应用。 |
(自 C++17 起) |
[编辑] 替代方案
在适用情况下,标签分派、if constexpr
(自 C++17 起)和 概念 (自 C++20 起) 通常优于使用 SFINAE。
|
(自 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++ 标准。
DR | 应用于 | 已发布的行为 | 正确行为 |
---|---|---|---|
CWG 295 | C++98 | 创建 cv 限定函数类型 可能导致替换失败 |
不再是失败, 丢弃 cv 限定 |
CWG 1227 | C++98 | 替换顺序未指定 | 与词法顺序相同 |
CWG 2054 | C++98 | 部分特化中的替换未正确指定 | 指定 |
CWG 2322 | C++11 | 不同词法顺序中的声明将导致模板 实例化以不同的顺序发生或根本不发生 |
这种情况下是非法的, 不需要诊断 |