Item 26: Avoid overloading on universal references.
Effective Modern C++ Item 26 的学习和解读。
这一节给出的建议是尽量不要对万能引用参数的函数进行重载,根因是重载函数的匹配规则。先从一个例子说起:
std::multiset<std::string> names; // global data structure
void logAndAdd(const std::string& name)
{
auto now = std::chrono::system_clock::now(); // get current time
log(now, "logAndAdd"); // make log entry
names.emplace(name); // add name to global data structure; see Item 42 for info on emplace
}
上面的代码,我们看3个调用:
std::string petName("Darla");
logAndAdd(petName); // pass lvalue std::string
logAndAdd(std::string("Persephone")); // pass rvalue std::string
logAndAdd("Patty Dog"); // pass string literal
第一个调用:logAndAdd 的参数 name 被绑定到一个左值 petName 上。由于 name 是一个左值,names.emplace(name) 将发生一次拷贝。
第二个调用:std::string(“Persephone”) 首先会显示构造出一个临时的 std::string,并且是一个右值。name 被绑定到一个右值,但是 name 是一个左值,names.emplace(name) 将发生一次拷贝。
第三个调用:“Patty Dog” 传入 logAndAdd 将隐式构造出一个临时的 std::string,并且是一个右值。name 被绑定到一个右值,但是 name 是一个左值,names.emplace(name) 将发生一次拷贝。
后面两个调用点, name 都是绑定到一个右值,我们可以通过移动来代替拷贝来提高性能,我们很容易使用万能引用重写 logAndAdd 如下:
template<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");
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 of copying a temporary std::string
现在,步入本节的主题。对于上述代码,假设在 logAndAdd 内部需要根据一个索引查找 name,logAndAdd 被重载成这样:
std::string nameFromIdx(int idx); // return name corresponding to idx
void logAndAdd(int idx) // new overload
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
template<typename T> // as berfore
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
新增一个 int 类型参数的调用方式:
std::string petName("Darla");
logAndAdd(petName); // as before, these
logAndAdd(std::string("Persephone")); // calls all invoke
logAndAdd("Patty Dog"); // the T&& overload
logAndAdd(22); // calls int overload
以上还没什么问题,一切都还符合我们的预期。但是,考略下面的调用场景:
short nameIdx;
… // give nameIdx a value
logAndAdd(nameIdx); // error!
对于 short 类型的 nameIdx,我们期望的显示是调用 int 类型的 logAndAdd 重载。但事实却是这样:万能引用版本的 T 将被推导成 short,因而产生一个确切的匹配版本,然后在 names.emplace 时候会用 short 类型去构造 std::string,显然会报错。
在 C++ 中,以万能引用为参数的函数是最贪婪的函数,它能实例化出多数能够胜任的精确匹配版本,而这个例子中 short 需要做类型转换成 int 类型才会匹配到 int 类型的 logAndAdd。而 C++ 重载函数的匹配原则:如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本。因此这里会匹配到万能引用实例化出的版本。
再看万能引用构造函数的例子:
class Person {
public:
template<typename T>
explicit Person(T&& n) // perfect forwarding ctor;
: name(std::forward<T>(n)) {} // initializes data member
explicit Person(int idx) // int ctor
: name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
这里会有两个问题。首先,传一个除 int 外的整形类型(比如,std::size_t, short, long)将不会调用 int 版本的构造函数,而是调用万能l引用版本的构造函数,然后这将导致编译失败。然后还有一个更加糟糕的问题,根据 Item 17: Understand special member function generation. 介绍我们知道编译器将在合适的条件下生成 copy 和 move 类构造函数。Person 实际可能是下面这个样子:
class Person {
public:
template<typename T> // perfect forwarding ctor
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)
…
};
考虑下面的调用:
Person p("Nancy");
auto cloneOfP(p); // create new Person from p; this won't compile!
使用 p 去创建一个新的 Person,这里不会调用 Person 的拷贝构造函数,而会调用完美转发构造函数。这是因为 Person 的拷贝构造函数的参数是一个 const 类型的 ,而 p 是一个非 const 类型,并且完美转发构造函数会实例化出一个精确的匹配版本。当我们稍微改造下 p,就可以调用编译器生成的拷贝构造函数:
const Person cp("Nancy"); // object is now const
auto cloneOfP(cp); // calls copy constructor!
虽然完美转发构造函数也能实例化出一个精确函数签名的版本,但是 C++ 重载匹配会选择普通的重载版本。
当继承介入进来之后,问题将变得更加让人无法接受,我们看下这样的代码片段:
class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs) // copy ctor; calls
: Person(rhs) // base class
{ … } // forwarding ctor!
SpecialPerson(SpecialPerson&& rhs) // move ctor; calls
: Person(std::move(rhs)) // base class
{ … } // forwarding ctor!
};
这两个构造函数都会调用基类的完美转发构造函数,因为这两个构造函数给基类传入的都是 SpecialPerson 类型的参数,完美转发构造函数会实例化出精确匹配的版本,最后代码将无法编译通过。
总之,对万能引用参数函数进行重载是一个糟糕的设计,我们需要尽量避免。