命名空间
变体
操作

SFINAE

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 
 
 

"替换失败不是错误"


这条规则应用于函数模板的重载决议:当替换显式指定的或推导出的类型以作为模板参数时失败,该特化将从重载集合中丢弃,而不是导致编译错误。

此特性用于模板元编程。

目录

[编辑] 解释

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

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

替换发生在

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

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