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 | 不同词法顺序的声明将导致模板 实例化以不同的顺序发生或根本不发生 |
这种情况是不合规的, 无需诊断 |