第二章–2.1–函数重载机制

Aki 发布于 2022-11-22 251 次阅读


函数重载机制涉及三个阶段:名称查找、模板函数处理、重载决议。前两个阶段得到函数的候选集,最后一个阶段从候选集中选出最合适的版本。注意下面这里模板函数仅仅支持全特化,而模板类偏特化也支持。

namespace animal
{
	class Cat {};
	void feed(Cat* foo, int) {}
};

class CatLike{CatLike(animal::Cat*){} };

void feed(CatLike){}

template<class T>
void feed(T* obj, double)
{

}

template<>
void feed(animal::Cat*obj,double d){}

animal::Cat cat;  
feed(&cat, 1);  //调用的是哪一个函数???
	

名称查找、

编译器需要在feed(&cat,1)这个点找出所有与feed同名的函数声明和函数模板,名称查找过程分为三类:

  • 1. 成员函数名查找,当使用 . 或者→ 进行成员函数调用时候,名称查找位于该成员类中的同名函数。
  • 2. 限定名称查找,当使用限定符 :: 进行函数调用时,例如std::sort,则查找位于 :: 左侧的名称空间中的同名函数。
  • 3. 未限定名称查找,除了上面两种,编译器还可以根据参数依赖查找规则(ADL)进行查找。

在上述代码1的例子中 feed(&cat,1) 属于未限定名称查找,由于cat是属于animal名称空间的,其名称空间中的同名函数也会纳入考虑范围(ADL发挥的作用),所以得到三个候选函数:

void animal::feed(Cat* foo, int);//1.
void feed(CatLike); //2.
template<typename T> void feed(T* obj,double);//3.

如果不想采用ADL规则,可以这样 (feed)(&cat,1) 调用将会删除第一个候选函数。

在名称查找过程中仅仅考虑普通函数和主模板函数,不会考虑特化版本,只有在第三阶段重载决议的时候才会考虑特化版本。

模板函数处理、

前一阶段的候选集中可能会出现模板函数,这个阶段是实例化模板函数从而可以被调用,模板参数推导成功之后,将会替换成推导后的类型。如:

template<typename T> void feed(T* obj,double);

经过处理(模板参数推导+替换)成为:

void feed(animal::Cat* ,double);

期间,如果模板参数推导与参数替换失败(SFINAE),则从候选集中删除,那么什么情况下模板参数会替换失败呢?考虑下面的模板函数。

template<class T>
void feed(T*obj,typename T::value_type v){}

该模板的第二个形参类型为 typename T::value_type,因为编译器不知道 T::value_type 是一个静态成员变量还是一个类型别名,因此需要前置 typename 关键字修饰用来解除类型与值的歧义,明确告诉编译器这是一个类型名称。如果写成 T::value_type 则指明这是一个成员变量,无需任何修饰。

首先编译器会根据实际调用情况将模板参数T推导为animal::Cat,接下来发生模板参数替换过程,从而得到函数签名:

void feed<animal::Cat>(animal::cat*obj,typename animal::Cat::value_type v){}

根据Cat的定义,并不存在Cat::value_type,因此这个替换失败,但并不会导致编译错误,编译器只是将其 从候选集中删除而已。这个过程被称为 substitution failure is not an errer,简称为 SFINAE。利用这一点程序员可以控制编译器的决策行为,选择预期的版本,同时也能够实现编译时的分支判断能力,在元编程中大量使用这种技巧。

重载决议、

在候选集中进行重载决议分为两个阶段:1. 规约可行函数集; 2. 挑选最佳可行函数;

首先从候选集中挑选函数,根据函数调用的实参与候选函数的形参的数量进行规约得到可行函数集,一个可行函数必须符号以下规则:

1)如果调用函数有M个实参,那么可行函数必须得有M个形参。

2)如果候选函数少于M个形参,但最后一个参数是可变参数,则为可行函数。

3)如果候选函数多于M个形参,但从第M+1到最后的形参都有默认参数,则为可行函数。在挑选最佳可行函数时只考虑前M个形参。

4)从C++20起,如果函数有约束,则必须符合约束。

5)可行需要保证每个形参类型即使通过隐式转换后也能与实参类型对得上。

简而言之,可行函数就是形参数目对应得上且形参类型能够直接或间接转换后仍可与实参类型匹配得上,同时要符合约束。

经过以上5点规则,最后留下来第一个和第三个候选函数,这就是第一个阶段:规约可行函数集。

当前可行函数集如下:

void feed(Cat*obj,int){}  //1
void feed<animal::Cat>(animal::Cat::Cat*obj,double){} //2

第二阶段是挑选最佳可行函数(如果没有挑选出来,就会发生编译错误),挑选最佳可行函数的规则是:

  • 1. 形参与实参类型最匹配、转换最少的为最佳可行函数。
  • 2. 非模板函数优于模板函数。
  • 3. 若多于两个模板实例,那么最具体的模板实例最佳。C++定义了一系列比较规则来说明哪种模板更具体。
  • 4. c++20起,若函数拥有约束,则选择约束最强的那一个。同时C++标准定义了一系列约束比较规则来说明哪种约束更强。

根据第一条规则,第一个函数的形参和实参类型完全匹配,而可行函数第二个实参会发生类型转换,因为实参是int,形参是double。因此决策出最佳可行函数为前者,而无需进行后续规则的决策。

现在我们假设第一个可行函数的签名为下面这样:

void animal::feed (Cat*obj,double){}

由于我们传入第二个实参是int,因此都需要发生隐式类型转换为double,那么第一条规则无法简单决策出最佳可行函数,而根据第二条规则,非模板函数由于模板函数,同样可以决策出最佳可行函数。更进一步如果第二个可行函数模板函数实例化签名为 void feed<animal::Cat>(animal::Cat*obj,double){} ,也就是类型完全匹配,那么根据第一条规则,就能够决策出最佳可行函数为该模板函数。

由于函数模板不能够偏特化,若多与两个模板实例,那么最具体的函数模板实例最佳,考虑如下模板函数。

template<class T> void feed(T*obj,double){}
template<class T> void feed(T obj,double){}

由于函数模板不支持偏特化,这两个为完全独立的函数模板即函数重载,在模板处理阶段会各自产生两个实例化模板。

void feed<animal::Cat>feed(animal::Cat*,double){}   //T = animal::Cat
void feed<animal::Cat*>feed(animal::Cat*,double){}   //T = animal::Cat*

两者的唯一区别是第一个模板函数只接受指针类型,而第二个模板可以接受任意类型(包含指针类型),那么第一个模板函数比第二个模板函数更加具体,因为第一个模板函数能够接受的类型第二个同样能够接受,反之不行。第一个模板函数包含于第二个模板函数,从而形成一种偏序关系,能够比较两者之间的大小(具体程度)。对于函数调用来说,将决策出第一个为最佳可行函数。

特化版本函数优于非特化版本函数。

注意事项、

根据C++标准,函数重载机制并不将函数的返回类型纳入考虑,其范围仅是函数签名中的函数名与函数形参类型。换句话说只靠返回值类型上的差异将无法进行重载决策。很大一部分原因是因为返回值是可选的,程序员可以考虑处理返回值也可以考虑不处理,在加上C++复杂的隐式类型转换规则,编译器无法决策该使用哪一个版本的函数。

C++的函数重载机制相当复杂,我们只需要掌握上述这个富有启发性的例子变能应对大多数重载场景了。