8. SFINAE¶
SFINAE 是 Substitution Failure Is Not An Error ( 替换失败不是一个错误 ) 的缩写。
在 C++11 之前,这并不是一个正式的 C++ 规范术语。而是由 David Vandevoorde 在其书籍
C++ Templates: The Complete Guide
首次命名的概念。
整个术语有三个关键词:
替换
失败
错误
如果不能理解它们背后的准确含义,就很难正确的理解并应用 SFINAE 。
8.1. 函数重载¶
之所以会存在 SFINAE ,关键在于两点:
C++ 允许函数重载;
C++ 有函数模版;
没有这两点,SFINAE 也无从谈起。
而允许 函数重载 ,就意味着,同一个函数名,可能存在多个版本的定义。
因而,当你的代码中出现 函数调用 时,编译器需要弄明白,此 函数调用 究竟调用的是那个版本。而这个弄明白的过程, 被称作 Overload Resolution ( 重载决议 ) 。
其大致的过程如下:
编译器首先会根据 C++ 规范所定义的 名字查找 规则,找到所有符合名字查找规则的同名函数,作为 候选集 。如果 候选集 为空,编译器直接报错。
将明确不匹配的版本(比如参数个数不匹配)踢出 候选集 。
如果剩余的 候选集 里存在 函数模版 ,则需要对 模版参数 进行推演(如果调用时用户没有全部明确指定的话)。 如果类型推演失败,则将此函数模版移出 候选集 。 如果类型推演成功,则将指定的,或推演出的类型,对模版参数进行 替换 ( Substitution )。 SFINAE 正是发生在这个环节:如果替换失败,编译器不会给出任何诊断信息,只是简单的将这个 函数模版 踢出 候选集 。如果替换成功,此模版函数 就被实例化为一个普通函数。( 函数模版 自身并不是 函数 )
到这一步依然还剩下的 候选集 ,被称作 viable candidates ( 可行候选集 )。编译器下一步的任务是从 可行候选集 中 找到 最佳匹配 的版本。而这可能会导致三种结局:
找到了 最佳匹配 版本。编译器将选择这个版本。
可行候选集 为空。这将导致编译错误,编译器会抱怨找不到合适的定义。
存在超过一个 最佳匹配 版本。这会导致 二义性 ,也会造成编译错误。
如果找到了 最佳匹配 版本,编译器还会进行其它检查(可见性,是否被声明为
=delete
等等)。
从这个过程我们就可以明确 SFINAE 的含义:某个函数调用的 候选集 中,如果存在 模版函数 ,则会对模版参数进行推演并替换, 而 替换失败 不会直接导致 编译错误 ,只会导致编译静悄悄地将此 模版函数 从 候选集 中移出。
8.1.1. 可行候选集¶
能够进入 可行候选集 的候选函数必须满足如下特征:
参数个数必须匹配。如果函数调用时程序员提供了 M 个参数,则候选函数参数个数必须属于如下三种情况之一:
有 M 个参数;
少于 M 个参数,但最后一个是变参;
多于 M 个参数,但第 M+1 及随后的参数都有默认值(或是变参)。
对于每一个 形参 ( parameter ),调用时对应的 实参 ( argument )都可以通过 隐式转换 ( implicit conversion ) 转换为对应的 形参 类型。如果 形参 是引用类型,那么右值引用实参不能 绑定到 non-const 左值引用形参;左值引用实参不能绑定到右值引用形参。
8.1.2. 函数匹配度¶
为了选出 最佳匹配 版本,可行候选集 里任意两个函数必须能够对比 函数匹配度 。
假设我们有两个函数 A 与 B ,
A 至少有一个参数比 B 更匹配,B 没有任何参数比 A 更匹配,则 A 比 B 更匹配;
如果 A 与 B 的参数匹配度一样, A 不是函数模版,但 B 是,则 A 比 B 更匹配;
如果 A 与 B 都是函数模版,但 A 比 B 更特化,则 A 比 B 更匹配。
8.1.3. 参数匹配度¶
上述规则中的第一条,是通过对比每一个 参数匹配度 ,来决定 函数匹配度 。而 参数匹配度 的对比规则如下:
标准转换 > 用户自定义转换 (通过
operator
定义的转换函数) > 变参 。如果都是 标准转换 ,则
如果 A 的转换序列是 B 的转换序列的子序列,则 A 比 B 更匹配;否则,
精确匹配 > 数值向宽转换 > 类型转换 。
8.1.4. 等价的函数模版¶
两个函数模版是等价的,如果:
定义在相同的 scope 内;
同名;
模版参数等价,即:
模版参数个数一致;
每一对对应的模版参数的 kind 相同(都是类型,值,模版, 变参);
如果某个参数是值,则类型应该相同;
如果某个参数是模版,模版应该等价;
包含了模版参数的返回值类型和函数参数类型的表达式应该等价;