Item 26: Avoid overloading on universal reference
之前看书看到了25,决定写读书笔记,所以从26开始,全书完本后回补之前的。写笔记感觉比只看书效果好很多,虽然用了更多时间,但是确实非常有收获。开始正题。
上例子:
std::multiset<std::string> names;
void logAndAdd(const std::string& name)
{
auto now = std::chrono::system_clock::now();
log(now, “logAndAdd”);
names.emplace(name);
}
std::string petName(“Darla”);
logAndAdd(petName);
logAndAdd(std::string(“Persephone”));
logAndAdd(“Patty Dog”);
第一个调用,参数name被绑在petName上,因为name(传递petName)是左值,所以最终被复制到了names里面。
第二个调用,参数name被绑定在右值上,但是因为name本身是左值,所以复制进names。
第三个调用,参数绑定右值,是一个临时字符串,相似,复制进names。
以上函数用左值绑定,所以无论输入左值或者右值,最终只能进行复制,效率低。因此创建一个模板函数来接受综合引用,如下:
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);
logAndAdd(std::string("Persephone"));
logAndAdd("Patty Dog");
再来看三次调用,第一个参数推导为左值引用,复制进names,同上。
第二个参数推导为右值,move进names,
第三个直接在names里面建立字符串。
模板化以后,第二和第三个调用效率提高。
问题1:
如果使用者想用index对logAndAdd进行调用,如下:
std::string nameFromIdx(int idx);
返回一个名字。
对函数进行重载:
void logAndAdd(int idx)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace( nameFromIdx(idx));
}
使用方法如下:
std::string petName("Darla");
logAndAdd(petName);
logAndAdd(std::string("Persephone"));
logAndAdd("Patty Dog");
logAndAdd(22);
问题来了,函数接受一个int型,按理说,如果你使用一个整数相关类型,如short型,调用接受int参数的重载函数最合适,但是
short nameIdx;
logAndAdd(nameIdx);
会报错,看似以int为参数的重载更适合这次调用,但是实际上,这次调用用的是模板综合引用的重载函数的调用。
原文解释是:Functions taking universal references are the greediest function in C++.
理解过来就是,除非有安全适合的函数,否则,综合引用的函数的优先级最高。所以short类型的输入,会被综合引用的重载函数推导为short左值引用。最终强行将short类型往string的multiset中塞,必然报错。
问题2:
构造函数重载的例子
class 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;
}
和之前的例子一样,如果使用short创建对象,那么必然调用综合引用构造函数。
系统会自动生成copy和move构造函数:
Person(const Person& rhs);
Person(Person&& rhs);
在构造对象时
Person p(“Nancy”);
这个没问题,调用综合引用重载构造函数
auto cloneOfP(p);
这里就没法编译。
原因是本应该调用copy构造函数的这里,实际上优先选择了完美转发构造函数(即模板综合引用构造函数),为啥嘞?因为只有const Person& 类型的参数才最适合系统自动生成的copy构造函数,和前一个例子一个道理,没有完全适合的,就优先使用综合引用进行推导。
所以如果想调用copy构造函数咋办?这样:
const Person cp("Nancy");
auto cloneOfP(cp);
还有一个特例就是,假如模板构造函数同样能把输入的参数推导成和copy构造函数一样的参数咋办?
explicit Person(const Person& n);
假设这个是推导出来的构造函数
Person(const Person& rhs);
这个是系统copy构造函数。
这个时候会优先调用后者。调用规则就是如果模板和普通函数都match,优先使用普通函数。
问题3:
class SpecialPerson : public Person{
public:
SpecialPerson(const SpecialPerson& rhs)
: Person(rhs)
{}
SpecialPerson(const SpecialPerson&& rhs)
: Person(std::move(rhs))
{}
};
作为派生类的copy和move构造函数,理想上,调用基函数的copy和move函数最合适,但是实际上调用的却是完美转发构造函数。为啥嘞这又是? 原因是,派生类传递到基类的参数是const SpecialPerson& rhs 而不是const Person& rhs,理由如问题2,调用了模板函数。
总结
1. 避免重载综合引用。
2. C++的调用优先级为:参数完全match的普通函数,然后就是综合引用推导函数,所以想用相近的参数(例如用short,long类型的参数调用int函数)调用普通函数,过不了推导函数这一关。
另:水平有限,望读过此文发现有问题的不吝指教,感激。