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++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 295 | C++98 | 创建 cv-qualified 函数类型 可能导致替换失败 |
不再是失败, 丢弃 cv-qualification |
CWG 1227 | C++98 | 替换顺序未指定 | 与词法顺序相同 |
CWG 2054 | C++98 | 部分特化中的替换未正确指定 | 已指定 |
CWG 2322 | C++11 | 不同词法顺序的声明会导致模板 实例化以不同顺序发生或根本不发生 |
这种情况是不合法的, 不需要诊断 |