问题实例
假设当前需要一个以name作为参数的函数,该函数负责记录当前日期和时间,然后将name添加到一个全局数据结构中,它的一大可能实现如下所示:1
2
3
4
5
6std::multiset<std::string> names; // global data structure
void logAndAdd(const std::string& name){
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd"); // make log entry
names.emplace(name);
}
该程序能够运行,但存在性能上的提升空间,考虑下述三种调用情况:1
2
3
4std::string petName("Darla");
logAndAdd(petName); // pass lvalue std::string
logAndAdd(std::string("Persephone")); // pass rvalue std::string
logAndAdd("Patty Dog"); // pass string literal
在第一次调用中,name被绑定于一个左值,转入logAndAdd后,name最终被传入names.emplace。因为name是一个左值,因此最终它被copy至names,我们无法阻止这种拷贝,因为移动左值会导致局部变量(petname)的失效。
在第二次调用中,name被绑定于一个右值,但需要明确的是name自身是一个左值,因此它被copy至names,此处可用move代替copy以提升性能。
在第三次调用中,我们首先从const char *处构造了一个临时string对象,然后用name绑定了该右值,随后将name复制到names,可见这里做了大量的无用功,我们本应直接利用const char*在names中构建string。
解决方案
我们可以按照Item24、25中所提及的方法重写logAndAdd:1
2
3
4
5
6
7
8
9
10template<typename T>
void logAndAdd(T&& name){
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string petName("Darla"); // as before
logAndAdd(petName); // as before, copy lvalue into multiset
logAndAdd(std::string("Persephone")); // move rvalue instead of copying it
logAndAdd("Patty Dog"); // create std::string in multiset instead
更进一步的问题实例
事实上客户端并非总是能够直接获取name,有些客户端只拥有一个获取对应name的索引,针对这种情况,我们将logAndAdd重载:1
2
3
4
5
6std::string nameFromIdx(int idx); // return name corresponding to idx
void logAndAdd(int idx){
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
但这个重载版本的鲁棒性不太好:1
2
3short nameIdx;
… // give nameIdx a value
logAndAdd(nameIdx); // error!
问题剖析
当前存在两个重载版本,带有universal reference的版本可以轻松地推衍出short,然后实现精准匹配,然后雪崩,参数为int的版本根本说不上话。
我们可以认为以universal reference为参数的函数几乎可以与任何参数完成精准匹配(因为它几乎可以绑定至一切类型,不能匹配的几种情况可见Item30),这也说明重载以universal reference的函数并不可取。
转发函数与默认函数
我们设想一下当前存在一个class Person:1
2
3
4
5
6
7
8
9class Person {
public:
template<typename T>
explicit Person(T&& n):name(std::forward<T>(n)) {}
explicit Person(int idx):name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
类似于logAndAdd,当传入size_t或者short时直接可能会导致Person构造函数雪崩,不过当前问题比那个还要严重。原因在于重载函数的个数远比你肉眼所见的要多。Item17曾经提及在适当的条件下,C++将会自动生成copy constructor与move constructor,即使该class已经具备了能够实例化构造函数的模板。因此,我们可以认为Person实际上如下所示:1
2
3
4
5
6
7
8class Person {
public:
template<typename T>
explicit Person(T&& n):name(std::forward<T>(n)) {}
explicit Person(int idx); // int ctor
Person(const Person& rhs); // copy ctor(compiler-generated)
Person(Person&& rhs); // move ctor(compiler-generated)
};
考虑一下以下调用:1
2Person p("Nancy");
auto cloneOfP(p); // create new Person from p;this won't compile!
上述操作不会调用copy ctor,而是会调用转发构造函数,然后该函数将尝试使用Person对象p初始化Person的std::string数据成员。std::string没有接受Person类型的构造函数,因此触发雪崩。有人会疑惑编译器为什么不调用copy ctor,这是由C++重载函数调用准则决定的。
cloneOfP正在使用非const左值(p)进行初始化,这表明当前可以实例化出一个以non-const Person为参数的构造函数:1
2
3
4
5class Person {
public:
explicit Person(Person& n):name(std::forward<Person&>(n)) {}
explicit Person(int idx);
}
在调用语句中,1
auto cloneOfP(p);
p似乎既可以触发copy ctor也能触发实例化函数,但实际上调用copy ctor时需要为p添加const,而实例化函数则不需要这项要求,因此编译器选择最佳匹配方案:调用转发构造函数。
如果我们将p改为const,那么copy ctor则可被顺利调用:1
2const Person cp("Nancy"); // object is now const
auto cloneOfP(cp); // calls copy constructor!
此时同样会实例化一个以const Person&为参数的转发构造函数,但在C++重载规则中,非模板函数优先级总是高于模板函数,因此本次将调用copy ctor。
一旦引入继承,转发函数与自动生成的函数之间会产生更多的交互,例如:1
2
3
4
5class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs):Person(rhs){ … }
SpecialPerson(SpecialPerson&& rhs):Person(std::move(rhs)){ … }
};
需要注意的是,派生类的拷贝、移动构造函数不会调用它们的基类对应的此类函数,而是将调用基类的转发构造函数。原因在于传入的实参优先匹配于转发函数。
总结
本节并未描述问题解决方案,在下一节中我们将对此继续讨论。
- universal refernce作为参数的函数不适合被重载。
- 转发函数经常会干涉别的函数的调用,并可能会因此产生极为恶劣的后果。