命名空间
变体
操作

SFINAE

来自 cppreference.cn
< 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 起)
  • explicit 说明符中使用的所有表达式
(自 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 不同词法顺序的声明将导致模板
实例化以不同的顺序发生或根本不发生
这种情况是不合规的,
无需诊断