一.万能引用形参重载函数的问题
(一)当产生精确匹配时: C++的重载匹配是贪婪的,当形参为万能引用类型时,实例化过程中,它和几乎任何的实参类型都会产生精确匹配。
1. 根据重载匹配规则,精确匹配优先于类型转换的函数。一旦万能引用成为重载候选函数,就会吸引起大批的实参类型。因此,形参为万能引用的重载函数在匹配时会被调用。
2. 万能引用类型的重载函数在完美转发构造函数中的问题更为严重。因为对于非常量的左值类型而言,它一般会形成比复制构造函数更精确的匹配,而且还会劫持派生类中对基类的复制和移动构造函数的调用。
(二)当产生相等匹配时:若在函数调用时,一个模板函数和一个普通函数(非模板类型的函数)具备相等的匹配程度,则优先选用普通函数。
【编程实验】完美转发与重载函数的冲突
#include <iostream> #include <set> //for multiset #include <chrono> //for std::chrono::system_clock::now() using namespace std; using timepoint_t = std::chrono::system_clock::time_point; std::multiset<std::string> names; //全局数据结构 void log(timepoint_t now, const string& content){} string nameFromIdx(int idx) { return "abc";} //1.普通函数与完美转发函数构成的重载关系 //1.1 形参为string void logAndAdd(const std::string& name) { auto now = std::chrono::system_clock::now(); //取得当前时间 log(now, "logAndAdd"); names.emplace(name); cout << "void logAndAdd(const std::string& name)" << endl; } //1.2 形参为int:通过索引查找名字,并记录到names中 void logAndAdd(int idx) { auto now = std::chrono::system_clock::now(); //取得当前时间 log(now, "logAndAdd"); names.emplace(nameFromIdx(idx)); cout << "void logAndAdd(int idx)" << endl; } //1.3 形参为万能引用类型(即构成完美转发) template<typename T> void logAndAdd(T&& name) { auto now = std::chrono::system_clock::now(); //取得当前时间 log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); cout << "void logAndAdd(T&& name)" << endl; } //2. 构造函数与完美转发函数构成的重载关系 class Person { std::string name; public: template<typename T> explicit Person(T&& n):name(std::forward<T>(n)) //完美转发构造函数 { cout << "explicit Person(T&& n):name(std::forward<T>(n))"<< endl; } explicit Person(int idx) : name(std::move(nameFromIdx(idx))){} //形参为int的构造函数 /*以下两个特殊成员函数是编译器自动生成的,为了便于观察,罗列出来 Person(const Person& rhs); //复制构造函数(编译器自动生成) Person(Person&& rhs); //移动构造函数(编译器自动生成) */ }; class SpecialPerson : public Person { public: using Person::Person; //继承构造函数 /*复制构造函数,调用的是基类的完美转发函数! error, 因为rhs的类型为SpecialPerson,当调用Person(rhs)时Person类的模板函数 会产生一个比默认的构造函数更精确的匹配函数Person(SpecialPerson& n),但name的构造函数中并没有SpecialPerson的重载版本。*/ //SpecialPerson(const SpecialPerson& rhs): Person(rhs){} /*移动构造函数,调用的是基类的完美转发函数,而非默认的移动构造! error, 原因同上*/ //SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)){} }; int main() { //1. 普通函数与完美转发构成的重载关系 //1.1 调用普通函数时:void logAndAdd(const std::string& name) std::string petName("Darla"); logAndAdd(petName); //传递左值。由于petName是左值,会被复制到names中且无法避免!(一次构造) logAndAdd(std::string("Persephone")); //传递右值。创建string临时对象,由于name本身是左值,会并被复制到names中。(一次构造和一次复制) logAndAdd("Patty Dog"); //传递字符串字面量。先创建string临时对象,并被复制到names (一次构造和一次复制) //1.2 调用模板函数时:void logAndAdd(T&& name) logAndAdd(petName); //传递左值。一如此前 logAndAdd(std::string("Persephone")); //传递右值。创建string临时对象,会并被移动到names中。(一次构造和一次移动) logAndAdd("Patty Dog"); //传递字符串字面量。将const char[10]&传递给names,(在multiset中直接构造,仅一次构造!!!) logAndAdd(22); //调用void logAndAdd(int idx) short nameIdx = 0; //logAndAdd(nameIdx); //编译失败,因为形参为short,此时函数模板会产生比logAndAdd(int idx)更精确的匹配函数:void logAndAdd(short idx) //从而,转去调用模板实例化后的void logAndAdd(short idx)函数,当模板中调用names.emplace(std::forward<T>(name))时 //会将nameIdx这个short类型的实参传给names中的emplace函数,但其并没有形参为short类型的重载函数,因此报错。(注意, //尽管nameIdx可以通过类型提升转化为int类型,从而匹配logAndAdd(int idx)函数,但根据C++匹配的贪婪性,精确匹配优先于 //类型提长后的匹配函数。 //2. 构造函数与完美转发构成的重载关系 Person p("Nancy"); //auto cloneOfP(p); //error,相当于Person cloneOfP(p);本意要调用默认的复制构造函数,但由于p是非const左值,此时模板会生成更精确匹配的 //Person(Person& n):name(std::forward<Person&>(n))构造函数。这里会将Person对象传入string的构造函数,因此报错。 const Person p2("Nancy"); auto cloneOfP(p2); //ok,调用默认的复制构造函数Person(const Person& p);虽然此时模板函数也会实例化出一个与复制构造函数签名相同的函数。 //但根据C++重载匹配规则,当具有相等的匹配程度时,普通函数优先于模板函数调用。 return 0; }
二. 替代方案
(一)放弃重载
1. 方法:重命名函数名称,放弃重载。如将logAndAdd函数改为logAndAddName和logAndAddNameIdx两个版本的函数。
2. 不足:不适用于构造函数处理,因为构造函数的名称是由语言固化的。
(二)传递const T&类型的形参
1. 方法:使用传递左值常量类型来代替万能引用类型。如将logAndAdd(T&& name)替换为logAndAdd(const string& n)。
2. 不足:达不到使用万能引用类型的高效率。
(三)传值
1. 方法:将按引用传递改为按值传递参数。(如将Person(T&& n)构造函数改为Person(string n)
2. 注意事项:只有在肯定会发生复制形参时,才考虑使用按值传递。
(四)标签分派
1. 方法:函数形参中除了万能引用类型外,再设置一个非万能引用的形参作为“标签”,利用该标签的差异进行函数分派。如本例中logAndAdd是向外提供的接口函数,这是一个“单版本”(无重载版本)的函数,它把待完成的工作分派到重载的实现函数(logAndAddImpl),logAndAddImpl利用了第2个形参充分的差异性进行分派,其中的 std::true_type和std::false_type就是所谓的“标签”。
2. 特点:
①重载函数接受一个万能引用形参,还有一个“标签”。在重载匹配时,不仅对这个万能引用类型有依赖,还对标签有依赖。标签值才决定了调用哪个重载版本。
② “放弃重载”、“传递const T&类型的形参”、“传值”等3种方法都放弃使用万能引用类型,但标签分派既可使用万能引用,又没有放弃重载。
3. 不足:
①构造函数比较特殊,如果只编写一个构造函数,然后在其中运用标签分派,那么有些针对构造函数调用就可能会被默认的构造函数接手处理,从而绕过标签分派系统。
②复制非常量左值时,总会调用万能引用类型构造函数。如果基类只声明一个完美转发构造函数,派生类以传统方式实现复制和移动构造函数时,总会调用到基类的完美转发构造函数,但正确的行为应该是调用到基类的复制或移动构造函数。
(五)对接受万能引用的模板施加限制
1. 方法:利用std::enable_if,只有满足其指定的条件时才启用该函数模板。
2. 特点:利用完美转发,效率高。还可以控制万能引用和重载的组合,而非简单地禁用之。该技术可以应用于重载无法避免的场合(如构造函数)。
三. 方案的权衡
(一)前三种技术(“放弃重载”、“传递const T&类型的形参”、“传值”)都需要对待调用的函数形参逐一指定类型。而后两种技术(标签分派和模板限制)则利用了完美转发,因此无须指定形参类型。
(二)完美转发效率更高。但也有不足:
1. 针对某些类型无法实施完美转发(如大括号初始化列表、0或NULL等)
2. 万能引用被转发的次数越多,出错时的错误信息就越多,甚至很难理解。
3. 万能引用形参通常在性能方面具有优势,但易用性方面一般有劣势。
(三)利用std::enable_if对模板施加限制,可以将万能引用和重载一起使用。但只有在编译器能使用万能引用重载的地方才能起作用。
(四)应避免依万能引用类型进行重载
【编程实验】替代方案
#include <iostream> #include <set> //for multiset #include <chrono> //for std::chrono::system_clock::now() using namespace std; using timepoint_t = std::chrono::system_clock::time_point; std::multiset<std::string> names; //全局数据结构 void log(timepoint_t now, const string& content){} string nameFromIdx(int idx) { return "abc";} //1. 普通函数与完美转发构成的重载(解决方案:标签分派) //1.1 前向声明 template<typename T> void logAndAdd(T&& name); //1.2 重载的实现函数 template<typename T> void logAndAddImpl(T&& name, std::false_type) //将第2个形参作为标签,利用其差异性实施分派 { auto now = std::chrono::system_clock::now(); //取得当前时间 log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); cout << "void logAndAddImpl(T&& name, std::false_type)" << endl; } //1.3 重载的实现函数 template<typename T = int> void logAndAddImpl(int idx, std::true_type) //将第2个形参作为标签,利用其差异性实施分派 { logAndAdd(nameFromIdx(idx)); cout << "void logAndAddImpl(int idx, std::true_type)" << endl; } //1.4 根据T的类型进行分派 template<typename T> void logAndAdd(T&& name) //函数的声明仍然保持不变 { //分派的目标函数 logAndAddImpl(std::forward<T>(name), std::is_integral<typename std::remove_reference<T>::type>()); } //2. 构造函数与完美转发函数构成的重载(解决方案:使用传值或enable_if) class Person { std::string name; public: //第三种方法:传值 //explicit Person(std::string n) :name(std::move(n)) //替换掉完美转发T&&类型的构造函数 //{ // cout << "explicit Person(std::string n):name(std::move(n))" << endl; //} //第五种方法: 使用std::enable_if来启用函数模板 //仅当T不是Person类型时才启用该构造函数。(如,在复制对象时避开该函数,转而去调用Person(const Person&)) //template<typename T, typename = typename std::enable_if< //decay可以去除T的引用和cv修饰符。 // !std::is_same<Person, typename std::decay<T>::type>::value> // ::type> //explicit Person(T && n) { cout << "Person(T&& n): T != Person" << endl; } //接受std::string类型以及可以转化为std::string类型实参类型的构造函数。 template<typename T, typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value //T为非Person及其子类 && !std::is_integral<std::remove_reference_t<T>>::value> > //T为非整型 explicit Person(T&& n):name(std::forward<T>(n)){ cout << "Person(T&& n)" << endl; } //接受实参为整型的构造函数 explicit Person(int idx) : name(std::move(nameFromIdx(idx))) { cout << "Person(int idx)" << endl; } //同前一个例子 /*以下两个特殊成员函数是编译器自动生成的,为了便于观察,罗列出来 Person(const Person& rhs); //复制构造函数(编译器自动生成) Person(Person&& rhs); //移动构造函数(编译器自动生成) */ }; class SpecialPerson : public Person { public: using Person::Person; //继承构造函数 /*复制构造函数:基类的完美转发函数被限制,会转而调用我们想要的默认构造函数!*/ SpecialPerson(const SpecialPerson& rhs): Person(rhs){} //以传统的方式实现复制构造 /*移动构造函数:基类的完美转发函数被限制,会转而调用我们想要的默认构造函数*/ SpecialPerson(SpecialPerson&& rhs) noexcept : Person(std::move(rhs)){}//以传统的方式实现移动构造 }; int main() { //1. 普通函数与完美转发构成的重载关系 std::string petName("Darla"); logAndAdd(petName); //传递左值。由于petName是左值,会被复制到names中且无法避免!(一次构造) logAndAdd(std::string("Persephone")); //传递右值。创建string临时对象,由于name本身是左值,会并被复制到names中。(一次构造和一次复制) logAndAdd("Patty Dog"); //传递字符串字面量。先创建string临时对象,并被复制到names (一次构造和一次复制) logAndAdd(20); //2. 构造函数与完美转发构成的重载关系 Person p("Nancy"); auto cloneOfP(p); //ok,相当于Person cloneOfP(p);调用默认构造函数 SpecialPerson sp("SantaClaus"); SpecialPerson sp2(sp); //ok,调用基类默认构造函数 SpecialPerson sp3(10); //ok,调用基类Person(int idx) return 0; }