https://zh.cppreference.com/w/cpp/language/sfinae
https://zh.cppreference.com/w/cpp/language/overload_resolution
https://zh.cppreference.com/w/cpp/language/adl
实参依赖查找
下面的重载决议中提到了实参依赖查找,看一下
实参依赖查找(Argument-Dependent Lookup,简称 ADL),也叫 Koenig 查找,是一种特殊的名字查找规则,用于在调用函数模板或非成员函数时,自动在实参所属的命名空间中查找对应的函数
当你调用一个函数时,如果这个函数没有用完整限定名(例如 ns::foo()
),那么编译器会到实参的命名空间中查找同名函数
#include <iostream>
namespace myns {
struct MyType {};
void print(MyType) {
std::cout << "myns::print called\n";
}
} // namespace myns
int main() {
myns::MyType obj;
print(obj); // 这里没有 using myns::print,也没写 myns::print(),但编译器能找到它
}
它可以用在操作符重载中:
namespace math {
struct Vector {};
Vector operator+(const Vector&, const Vector&) {
return {};
}
}
math::Vector a, b;
auto c = a + b; // 依靠 ADL 找到 operator+
常与模板函数搭配使用,例如:
namespace myns {
struct Widget {};
void swap(Widget&, Widget&) {
std::cout << "custom swap\n";
}
}
template<typename T>
void doSomething(T& a, T& b) {
using std::swap;
swap(a, b); // 借助 ADL 找到 myns::swap
}
注意:
- ADL 只影响无限定的名字查找
- 如果无限定的名字查找找到的候选集合中含有:类成员的声明、块作用域的函数声明、任何非函数或函数模板的声明时,无法使用 ADL
重载决议
重载决议通过以下几个步骤进行:
- 建立候选函数集合
- 从候选函数集合中去除函数,只保留可行函数
- 分析可行函数集合,以确定唯一的最佳可行函数
如果函数无法被重载决议选择,那么不能使用它
候选函数
在重载决议开始前,将名字查找和模板实参推导所选择的函数组成候选函数 的集合
- 调用具名函数
- 调用类对象(可调用对象)
- 调用重载运算符
- 使用构造函数进行直接初始化或默认初始化时,候选函数是所有的构造函数
- 进行拷贝初始化时,候选函数是所有的转换构造函数等
还有一些其他的东西
可行函数
给定以上述方式构造的候选函数集,重载决议的下一步骤是检验各个实参与形参,并将集合缩减为可行函数 的集合
候选函数必须满足下列条件:
- 实参数量相等的可行
- 实参数量少的,如果有省略号形参,那么可行
- 实参数量多的,如果剩下的有默认实参,那么可行
- 如果有关联的约束(C++ 20),那么必须满足它
- 对于每个实参,必须至少存在一个隐式转换序列将它转换到对应的形参(用户定义的转换只能隐式应用一次)
- 如果任何形参具有引用类型,那么这一步负责引用绑定:如果右值实参对应非 `const1 左值引用形参,或左值实参对应右值引用形参,那么函数不可行
如果有两个函数 F1
和 F2
可行,那么如果满足下面的条件,就称 F1
优于 F2
(从前往后依次判断):
- 至少存在一个
F1
的实参,它的隐式转换优于F2
的该实参的对应的隐式转换 - 从
F1
的返回类型到要初始化的类型的标准转换序列优于从F2
的返回类型到该类型的标准转换序列 - 在需要对返回值进行引用绑定的情况下,
F1
的返回类型是与正在初始化的引用相同种类的引用(左值或右值),而F2
的返回类型不是 F1
是非模板的函数而F2
是模板特化F1
与F2
都是模板特化,且按照模板特化的偏序规则,F1
更特殊F2
是重写的候选而F1
不是(也就是说重写的成员函数劣于普通的成员函数)F1
是从用户定义推导指引所生成的而F2
不是F1
是复制推导候选而F2
不是F1
是从非模板构造函数生成而F2
是从构造函数模板生成(构造函数也有正常优于模板的说法)
总结:
- 参数列表的隐式转换分级、返回值的隐式转换
- 非模板优于模板,模板特化越特殊越好
- 非重写成员函数优于重写成员函数
- 对构造函数来说,先看参数,再看推导指引,然后非模板优于模板
隐式转换序列的分级
每种标准转换序列的类型都被赋予三个等级之一:
- 准确匹配:不要求转换、左值到右值转换、限定性转换、函数指针转换、(C++ 17 起)类类型到相同类的用户定义转换
- 提升:整数提升、浮点数提升
- 转换:整数转换、浮点数转换、浮点数整数转换、指针转换、成员指针转换、布尔转换、派生类到它的基类的用户定义转换
下面的不看了,像法律条文一样
SFINAE
SFINAE 就是 Substitution Failure Is Not An Error 的首字母缩写,意思是“替换失败不是错误”
在函数模板的重载决议中,当模板形参在替换成显式指定的类型或推导出的类型失败时,从重载集中丢弃这个特化,而非导致编译失败
在选择候选函数之前,会对函数模板形参进行两次替换:
- 显式指定的模板实参
- 推导出的实参和从默认项获得的实参
替换会发生在:函数形参类型、返回值类型、模板形参声明(尖括号里面的东西)等
下面类型的错误是 SFINAE 错误:
同时展开多个不同长度的参数包
试图构建
void
、引用或者函数的数组或者异常长度(长度小于等于 0)的数组:template<int I> void div(char(*)[I % 2 == 0] = 0) { // 当 I 是偶数时选择这个重载,当 I 是奇数时,数组长度为 0 } template<int I> void div(char(*)[I % 2 == 1] = 0) { // 当 I 是奇数时选择这个重载 }
试图在作用域解析运算符
::
左侧使用类和枚举以外的类型:template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // 使用第二个重载
试图使用类型的成员,其中类型不包含指定成员或者在要求类型、模板、非类型的地方,指定成员不是类型、模板、非类型
试图创建指向引用的指针
试图创建到
void
的引用试图创建指向
T
的成员的指针,其中T
不是类类型试图将非法类型给予非类型模板形参
一个特殊例子:
template<typename A>
struct B { using type = typename A::type; };
template<
typename T,
typename U = typename T::type, // 如果 T 没有成员 type 那么就是 SFINAE 失败
typename V = typename B<T>::type> // 如果 T 没有成员 type 那么就是硬错误
// (经由 CWG 1227 保证不出现,
// 因为到 U 的默认模板实参中的替换会首先失败)
void foo (int);
template<typename T>
typename T::type h(typename B<T>::type);
template<typename T>
auto h(typename B<T>::type) -> typename T::type; // 重声明
template<typename T>
void h(...) {}
using R = decltype(h<int>(0)); // 非良构,不要求诊断
因为 B<T>
里面确实声明了 type
,而在替换阶段并没有进行实例化,所以在替换阶段并不知道这个 type
是否正常
在真正实例化了这个模板函数时,B<T>
就被实例化,这时才发现没有 T::type
,所以是一个硬错误
而这里说到了 CWG 1227,这个问题编号表示:当多个默认模板参数有依赖关系时,替换顺序是从前往后依次进行,前一个失败则后面不替换。所以该例子实际上不会报错