前言一些与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,不就是选择更匹配的吗?也没那么容易。当你考虑到各种转换,像什么指针,引用的转换,左值引用,右值引用转换,模板实参推导,等等一系列的时候,情况变的糟糕了。。。(尤其是考虑转换)而且重载决议可能会选择错误的匹配,这时候需要你去调试。弄清楚编译器是怎样执行的 函数重载 && 函数模板
确实,这两个东西很相似。什么时候使用函数重载,什么时候使用函数模板呢?
当实现需要根据参数类型而变化时,使用函数重载。
当实现完全类似而与参数类型无关时,使用函数模板。
实际上,当我们谈论函数模板时,往往谈论的是函数重载而非函数模板。因为函数模板不支持偏特化。
在重载决议发生之前,编译器会进行一种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 templatevoid 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干掉。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)