命名空间
变体
操作

SFINAE

来自 cppreference.com
< cpp‎ | 语言
 
 
C++语言
一般主题
流程控制
条件执行语句
if
迭代语句(循环)
for
range-for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 指定符
动态异常说明 (直到 C++17*)
noexcept 指定符 (C++11)
异常
命名空间
类型
指定符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储持续时间指定符
初始化
 
 
 
 

"替换失败不是错误"


在函数模板的重载解析过程中,当将显式指定的或推断出的类型替换为模板参数失败时,该特化将从重载集合中丢弃,而不是导致编译错误。

此特性在模板元编程中使用。

目录

[编辑] 说明

函数模板参数被替换(用模板参数替换)两次

  • 显式指定的模板参数在模板参数推断之前被替换
  • 推断出的参数和从默认值获得的参数在模板参数推断之后被替换

替换发生在

  • 函数类型中使用的所有类型(包括返回值类型和所有参数的类型)
  • 模板参数声明中使用的所有类型
  • 部分特化的模板参数列表中使用的所有类型
  • 函数类型中使用的所有表达式
  • 模板参数声明中使用的所有表达式
  • 部分特化的模板参数列表中使用的所有表达式
(自 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。

static_assert 通常优于 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 不同词法顺序中的声明将导致模板
实例化以不同的顺序发生或根本不发生
这种情况下是非法的,
不需要诊断