8. SFINAE

SFINAESubstitution Failure Is Not An Error ( 替换失败不是一个错误 ) 的缩写。 在 C++11 之前,这并不是一个正式的 C++ 规范术语。而是由 David Vandevoorde 在其书籍 C++ Templates: The Complete Guide 首次命名的概念。

整个术语有三个关键词:

  1. 替换

  2. 失败

  3. 错误

如果不能理解它们背后的准确含义,就很难正确的理解并应用 SFINAE

8.1. 函数重载

之所以会存在 SFINAE ,关键在于两点:

  1. C++ 允许函数重载;

  2. C++ 有函数模版;

没有这两点,SFINAE 也无从谈起。

而允许 函数重载 ,就意味着,同一个函数名,可能存在多个版本的定义。

因而,当你的代码中出现 函数调用 时,编译器需要弄明白,此 函数调用 究竟调用的是那个版本。而这个弄明白的过程, 被称作 Overload Resolution ( 重载决议 ) 。

其大致的过程如下:

  1. 编译器首先会根据 C++ 规范所定义的 名字查找 规则,找到所有符合名字查找规则的同名函数,作为 候选集 。如果 候选集 为空,编译器直接报错。

  2. 将明确不匹配的版本(比如参数个数不匹配)踢出 候选集

  3. 如果剩余的 候选集 里存在 函数模版 ,则需要对 模版参数 进行推演(如果调用时用户没有全部明确指定的话)。 如果类型推演失败,则将此函数模版移出 候选集 。 如果类型推演成功,则将指定的,或推演出的类型,对模版参数进行 替换Substitution )。 SFINAE 正是发生在这个环节:如果替换失败,编译器不会给出任何诊断信息,只是简单的将这个 函数模版 踢出 候选集 。如果替换成功,此模版函数 就被实例化为一个普通函数。( 函数模版 自身并不是 函数 )

  4. 到这一步依然还剩下的 候选集 ,被称作 viable candidates可行候选集 )。编译器下一步的任务是从 可行候选集 中 找到 最佳匹配 的版本。而这可能会导致三种结局:

    • 找到了 最佳匹配 版本。编译器将选择这个版本。

    • 可行候选集 为空。这将导致编译错误,编译器会抱怨找不到合适的定义。

    • 存在超过一个 最佳匹配 版本。这会导致 二义性 ,也会造成编译错误。

  5. 如果找到了 最佳匹配 版本,编译器还会进行其它检查(可见性,是否被声明为 =delete 等等)。

从这个过程我们就可以明确 SFINAE 的含义:某个函数调用的 候选集 中,如果存在 模版函数 ,则会对模版参数进行推演并替换, 而 替换失败 不会直接导致 编译错误 ,只会导致编译静悄悄地将此 模版函数候选集 中移出。

8.1.1. 可行候选集

能够进入 可行候选集 的候选函数必须满足如下特征:

  1. 参数个数必须匹配。如果函数调用时程序员提供了 M 个参数,则候选函数参数个数必须属于如下三种情况之一:

    1. M 个参数;

    2. 少于 M 个参数,但最后一个是变参;

    3. 多于 M 个参数,但第 M+1 及随后的参数都有默认值(或是变参)。

  2. 对于每一个 形参 ( parameter ),调用时对应的 实参argument )都可以通过 隐式转换 ( implicit conversion ) 转换为对应的 形参 类型。如果 形参 是引用类型,那么右值引用实参不能 绑定到 non-const 左值引用形参;左值引用实参不能绑定到右值引用形参。

8.1.2. 函数匹配度

为了选出 最佳匹配 版本,可行候选集 里任意两个函数必须能够对比 函数匹配度

假设我们有两个函数 AB

  1. A 至少有一个参数比 B 更匹配,B 没有任何参数比 A 更匹配,则 AB 更匹配;

  2. 如果 AB 的参数匹配度一样, A 不是函数模版,但 B 是,则 AB 更匹配;

  3. 如果 AB 都是函数模版,但 AB 更特化,则 AB 更匹配。

8.1.3. 参数匹配度

上述规则中的第一条,是通过对比每一个 参数匹配度 ,来决定 函数匹配度 。而 参数匹配度 的对比规则如下:

  1. 标准转换 > 用户自定义转换 (通过 operator 定义的转换函数) > 变参

  2. 如果都是 标准转换 ,则

    1. 如果 A 的转换序列是 B 的转换序列的子序列,则 AB 更匹配;否则,

    2. 精确匹配 > 数值向宽转换 > 类型转换

8.1.4. 等价的函数模版

两个函数模版是等价的,如果:

  1. 定义在相同的 scope 内;

  2. 同名;

  3. 模版参数等价,即:

    1. 模版参数个数一致;

    2. 每一对对应的模版参数的 kind 相同(都是类型,值,模版, 变参);

    3. 如果某个参数是值,则类型应该相同;

    4. 如果某个参数是模版,模版应该等价;

  4. 包含了模版参数的返回值类型和函数参数类型的表达式应该等价;