C++重载决议

前言

这篇博客用来总结overload resolution,这是一篇关于重载决议的演讲,very nice。

一些与overload易混淆的概念

override

  • 子类继承父类,有可能你需要让父类某些函数为虚函数,让子类重写虚函数,形成多态。
  • 但是你的子类,可能虚函数写错了,就无法形成多态。你可以在子类虚函数上加上override,编译器会帮助你检查该函数是否重写了。
  • overload 是编译期多态,根据date type来区分调用哪个重载版本。
  • override是运行期多态,根据object的动态类型区分调用哪种method。

运算符重载

  • 运算符重载实际上不算是通常意义上的重载,它总是有一个operator加上一个要重载的运算符,这是它的名字。
  • 运算符重载实现的机制也是重载决议。

为什么需要重载决议?

  • 当多个函数对于某个调用都是可见的(visible),且它们具有相同的名字,不同的parameter-list(形参列表)时,重载就会发生。
  • 函数重载可以避免名字过长。比如你有你个函数想用作int,char:
doThingsToInt(); // 不使用函数重载,名字就可能很长
doThingToChar();

doThing(); //函数重载

声明函数重载

  • 函数重载适用范围很广,构造函数,成员函数,普通函数等等
  • 当两个函数:
  1. 对于当前函数调用都是可见的,
  2. 拥有相同的名字,不同的参数列表,
  3. 这两个函数互为重载。
  • 声明重载函数的顺序并不会改变调用的重载函数。你先声明的函数不一定优先级更高。
/* example 1*/
int dothing(int);         // one
int dothing(int, double); // two

int main(){
	dothing(1);			  //调用one
	dothing(1, 3.14);	  //调用two
}
  • 上面就是最普通的函数重载,它的调用也符合我们的预期。

什么是重载决议?

  • 重载决议就是选择最匹配的重载函数的过程。
  • 重载决议是面对函数重载时编译器必须做出的决定。
  • 重载决议完全发生在编译期。
  • 重载决议只会考虑实参的data type,和形参的type。该过程只关系类型的匹配程度,它不关心实际传过去的值!!只关心类型,只关心类型,只关心类型。
  • 如果两个函数拥有同样的rank(等级),编译器就无法挑选出更匹配的那个,这就ambiguous。

模板函数也参与重载决议。。。因为模板函数和非模板函数也能形参函数重载,
这让重载决议有点复杂,

  • 如果模板和非模板函数拥有相同的rank,那么非模板函数会被选择。(这也正常)

什么不是函数重载?

  • 我觉得这点也很简单,只要知道上面的几点就行。
  1. 当两个函数只有返回值不同时,无法形成重载。返回值不是必须的,也就是说你可以不使用返回值,这样无法看出你想选择哪一个。(我们可以通过SFINAE实现一些“返回值版的函数重载”,interesting)。
  2. 当两个函数签名式(signature,函数名字+参数列表)相同,但是有不同的default value时,也不会形成重载。因为函数重载只看类型,不会看实际值。
  3. 当两个函数签名式相同,但是一个为static时,也不会形成函数重载。。。。

重载决议之过程

  • 你可能认为重载决议very easy,不就是选择更匹配的吗?
  • 也没那么容易。当你考虑到各种转换,像什么指针,引用的转换,左值引用,右值引用转换,模板实参推导,等等一系列的时候,情况变的糟糕了。。。(尤其是考虑转换)
  • 而且重载决议可能会选择错误的匹配,这时候需要你去调试。弄清楚编译器是怎样执行的

函数重载 && 函数模板

  • 确实,这两个东西很相似。
  • 什么时候使用函数重载,什么时候使用函数模板呢?

当实现需要根据参数类型而变化时,使用函数重载。
当实现完全类似而与参数类型无关时,使用函数模板。
实际上,当我们谈论函数模板时,往往谈论的是函数重载而非函数模板。因为函数模板不支持偏特化。

before 重载决议

  • 在重载决议发生之前,编译器会进行一种procedure叫做name lookup(所谓名称查找,这是固定术语)。
  • name lookup会去找到关于你的调用可见的所有函数声明。
  1. name lookup 也许需要argumemt dependent lookup(所谓ADL,依据实参,编译器会自动去查看实参所在的命名空间,即使你没有显示说明。)
  2. 模板函数 也许需要 template argument deduction (模板实参推导,推导出函数模板的类型)。当编译器找到模板函数,它会去尝试推导出参数类型,然后放入overload set。
  • 所有可见的函数声明的列表形成一个集合,叫做overload set。注意,这一步形成的overload set可能是非常大的,因为它只是简单的查找名称,查找全局,然后是各种命名空间,实参命名空间等等。

more details

  • 第一步,编译器构造一个overload set,然后将其中所有的函数声明放入一个叫做candidates(候选人)的列表中。
  • 第二步,编译器会将那些非法的函数声明从candidates列表中去掉,所谓“非法”在C++标准中被叫做“not viable”。
  • 编译器一般会在两个方面判断一个函数是否为“not viable”:
  1. 根据你传入的参数个数。如果函数声明的参数个数比你调用的参数个数少,那么该函数声明not viable,将该函数声明从candidates列表移除。如果函数声明的参数个数多,且多余的参数没有default value,那么也是not viable。
  2. 判断参数类型。如果函数声明中的参数类型与实参的类型不匹配,即使考虑到隐式类型转换,那么就是not viable的。
void dothing(std::string);
void dothing(int);
void dothing(int, double);

int main(){
	dothing(3);      //调用第二个,因为第三个参数个数太多,第一个无法将int转换成std::string。
	dothing(3, 3.14);//调用第三个
	return 0;
}
  • 此时,在candidates列表中,所有的候选者都是可行的,但是我们要去寻找一个best match的。
  • 第三步,找到最匹配的候选者。我们要通过一系列C++标准对candidates列表进行rank(排名)。
  • 如果只有一个候选者获得最高的rank,或者candidates列表只剩下了一个候选者,那么排名结束,这就是我们想要的。如果有两个或者多个候选者排名相同,那么我们就要进入所谓的tie breaker(决胜局)。

type conversion(类型转换)

  • 实际上,重载决议的最终阶段还是回到了类型转换上来。
  • 类型转换,即将一种类型转换成另外一种类型。
  • 我们有显示转换,static_cast,const_cast等等,我们还有隐式转换,如float到int。

rank(排名)

  • 让我们通过标准来进行排序。

标准1,2,3
标准4,5

  • 上面就是5个转换的排名。
  • 第三个就是对非const到const 和非volatile到volatile的转换。
  • 第四个是所谓的整形提升。

栗子2:

void dothing2(char){};  // one
void dothing2(long){};   // two

int main(){
	dothing2(42);  //选择哪一个重载版本? one or two ?
	return 0;
}
  • 编译器认为这是ambiguous!!
  • but,why?标准说,整形提升的排名高于普通的转换,为什么不是选择int到long的“整形提升”?
  • 我们需要更仔细的查看标准,标准是说,整形提升是某一种长度比int小的intergral提升为int,你会发现整形提升往往是提升到int,而标准认为int到long不是整形提升,而是转换!!
  • 所有的整形提升必须在标准中,如果标准没有提到,那么就是转换!!

整形提升

在这里插入图片描述

其他两个排名

  • C++标准中还说了其他两个转换的排名。

其他的转换

  • user defined conversion是转换成class类型or从class类型转换出去,

  • 这意味着,即使你使用std里面的各种class,编译器也会认为它们是用户自定义类型。

  • 如果两个函数声明拥有同样的rank,那么它们需要进入tie breaker。而我们的tie breaker也有一些规则:

  1. 如果一个模板和一个非模板拥有同样的rank,那么选择非模板。
  2. 如果一个隐式转换需要的“steps”更少,那么我们选择这个。
  3. 对于C++20的concept新增一个tie breaker的规则:如果一个candidate的concept更严格,那么选择这一个。
  4. 注意,该3个规则不会影响rank,只有引入tie breaker的时候才会启用该三点规则。
  • 如果tie breaker同样不能分出胜负,那么就会报错。

栗子4:

void dothing4(char val){};  // one

template <class T>
void dothing4(T val){};    // two

int main(){
	dothing4(42);  //选择哪一个重载版本? one or two ?
	return 0;
}
  • 选择第二个版本。因为T会被实参推导规则推导成int,这是一个完美匹配。
  • 所以我们应该尽量避免将函数模板和普通函数放在一个overload set中,因为模板往往形成最佳匹配。
  • 注意,模板大于非模板只有在tie breaker中才会被使用。

解决ambiguous function call

  • 增加或者删除一个重载函数
  • 让构造函数成为implicit
  • 为模板函数的参数添加约束(C++20有concept),这样通过SFINAE就会帮助我们排除某些函数模板。
  • 将argument显示转换,而非使用隐式转换。

例如,static_cast<>,explicitly构造一个对象,使用string(“hello”)作为参数,而非传递一个string literal(string字面量)。

当最佳匹配不是你想要的

  • 尝试利用规则制造出ambiguous,然后就能推导出最佳匹配位于哪一个rank,然后就可以进行更细致的推导。
  • 尝试去理解编译器如何看待candidates
  • 更改某些arguments的类型,

栗子6:

void dothing6_A(double, int, int){}      // one
void dothing6_A(int, double, double){}   // two

dothing6_A(4, 5, 6);  //选择哪一个? one or two ?

void dothing6_B(int, int, double){}     // three
void dothing6_B(int, double, double){}  // four

dothing6_B(4, 5, 6);   //选择哪一个? three or four ?
  • dothing6_A的调用是ambiguous。你可能会选择one,因为第一个有两个int,可惜这不是编译器认为的。
  • 编译器每次都会为每个参数进行rank。第一回合,为第一个参数排序,int是更好的排序,所以two不能是最好匹配。第二回合,one取胜。所以two不可能是最好匹配。到此为止,编译器结束重载决议,没有最好匹配。(这类似与某些游戏,没有平局,只有双输。)
  • 而在dothing6_B中,第一回合,three和four的第一个参数都是int,它们是同样的rank。第二回合,three取胜,因为它的int更匹配。而第三回合,three和four又是同样的rank。综合以上,three在第二回合取胜,且其他回合没有更差,所以选择重载版本three。
  • amazing。

栗子7:

void dothing7_A(int&){}      // one
void dothing7_A(int){}       // two

int x = 42;
dothing7_A(x);  //选择哪一个? one or two ?

void dothing7_B(int&){}     // three
void dothing7_B(int){}      // four

dothing7_B(42);   //选择哪一个? three or four ?
  • 首先我们来看第一个调用,将int&绑定到int,是一个最佳匹配,不是一个转换,而按值传递同样是最佳匹配,所以该调用时ambiguous。
  • 第二个调用,选择four。因为42是一个常量,不能将非const左值引用绑定到常量上。因此four的调用不合法,只能选择three。

栗子8:

void dothing8_A(int&){}      // one
void dothing8_A(int&&){}       // two

int x = 42;
dothing8_A(x);  //选择哪一个? one or two ?

void dothing8_B(int&){}     // three
void dothing8_B(int&&){}      // four

dothing8_B(42);   //选择哪一个? three or four ?
  • 在第一个调用中,选择one。因为将右值引用绑定到左值上。所以two是非法的调用,所以one胜出。
  • 在第二个调用中,同样的,我们无法将非const左值引用绑定到常量上,所以four胜出。

栗子9:

void dothing9(int&){
		cout << "int&" << endl;   //one
	}
	void dothing9(...){
		cout << "..." << endl;    //two
	}

	struct MyStruct {
		int data_ : 5;
		};

	MyStruct obj;
	dothing9(obj.data_);  //调用哪一个?
  • 编译器会调用one。因为位域(bit field)在C++中不认为是一种type,位域不在type system中,所以当你传递一个位数为5的位域时,编译器会认为你传递了一个int类型,所以one是完美匹配。
  • However,当你真正编译的时候,编译器会报错,因为你无法将int&绑定到位域上。
  • 即使你加入一个const int&,依然会报错,因为const int&版本会在重载决议时被非const干掉。
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值