C++重载决议

C++重载决议,第1张

C++重载决议

目录

前言一些与overload易混淆的概念

override运算符重载 为什么需要重载决议

声明函数重载 什么是重载决议?

什么不是函数重载? 重载决议之过程

函数重载 && 函数模板before 重载决议more detailstype conversion(类型转换)rank(排名)其他两个排名解决ambiguous function call当最佳匹配不是你想要的

前言

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

一些与overload易混淆的概念 override

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

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

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

doThingsToInt(); // 不使用函数重载,名字就可能很长
doThingToChar();

doThing(); //函数重载
声明函数重载

函数重载适用范围很广,构造函数,成员函数,普通函数等等当两个函数:

    对于当前函数调用都是可见的,拥有相同的名字,不同的参数列表,这两个函数互为重载。

声明重载函数的顺序并不会改变调用的重载函数。你先声明的函数不一定优先级更高。

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,那么非模板函数会被选择。(这也正常) 什么不是函数重载?

我觉得这点也很简单,只要知道上面的几点就行。

    当两个函数只有返回值不同时,无法形成重载。返回值不是必须的,也就是说你可以不使用返回值,这样无法看出你想选择哪一个。(我们可以通过SFINAE实现一些“返回值版的函数重载”,interesting)。当两个函数签名式(signature,函数名字+参数列表)相同,但是有不同的default value时,也不会形成重载。因为函数重载只看类型,不会看实际值。当两个函数签名式相同,但是一个为static时,也不会形成函数重载。。。。
重载决议之过程

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

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

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

before 重载决议

在重载决议发生之前,编译器会进行一种procedure叫做name lookup(所谓名称查找,这是固定术语)。name lookup会去找到关于你的调用可见的所有函数声明。

    name lookup 也许需要argumemt dependent lookup(所谓ADL,依据实参,编译器会自动去查看实参所在的命名空间,即使你没有显示说明。)模板函数 也许需要 template argument deduction (模板实参推导,推导出函数模板的类型)。当编译器找到模板函数,它会去尝试推导出参数类型,然后放入overload set。

所有可见的函数声明的列表形成一个集合,叫做overload set。注意,这一步形成的overload set可能是非常大的,因为它只是简单的查找名称,查找全局,然后是各种命名空间,实参命名空间等等。 more details

第一步,编译器构造一个overload set,然后将其中所有的函数声明放入一个叫做candidates(候选人)的列表中。第二步,编译器会将那些非法的函数声明从candidates列表中去掉,所谓“非法”在C++标准中被叫做“not viable”。编译器一般会在两个方面判断一个函数是否为“not viable”:

    根据你传入的参数个数。如果函数声明的参数个数比你调用的参数个数少,那么该函数声明not viable,将该函数声明从candidates列表移除。如果函数声明的参数个数多,且多余的参数没有default value,那么也是not viable。判断参数类型。如果函数声明中的参数类型与实参的类型不匹配,即使考虑到隐式类型转换,那么就是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(排名)

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


上面就是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也有一些规则:

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

如果tie breaker同样不能分出胜负,那么就会报错。

栗子4:

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

template 
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 ?

one 和 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干掉。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5714827.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存