条款26:避免在万能引用上重载
条款26:避免在万能引用上重载
背景示例
假设有个函数,需要用名字打印日志并加入全局集合:
std::multiset<std::string> names;
// const 左值引用既能绑定左值,也能绑定右值(包括临时对象)
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"); // 字符串字面量,创建临时拷贝
-
logAndAdd(petName);petName是一个已有的std::string对象,是左值,绑定到const std::string&上,没发生复制,直接传引用。- 但是后续调用
names.emplace(name);中,emplace需要构造std::string对象,由于name是左值(参数本身是引用),它会调用拷贝构造函数拷贝一份到容器中。 - 所以这里是拷贝发生在
emplace内部。
-
logAndAdd(std::string("Persephone"));- 这里传入的是一个临时右值
std::string,但是函数参数是const std::string&,它可以绑定右值的 const 引用。 - 参数
name本身是左值(因为有名字),所以names.emplace(name)依然调用拷贝构造。 - 虽然传入的临时对象本质是右值,但函数参数变成左值引用,导致后续仍调用拷贝构造。
- 这里传入的是一个临时右值
-
logAndAdd("Patty Dog");- 这里传入的是字符串字面量(
const char*),先会隐式构造一个临时std::string绑定给参数name(const std::string&),然后emplace(name)又调用拷贝构造拷贝进集合。
- 这里传入的是字符串字面量(
优化:用万能引用+完美转发
template<typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
- 左值传入时,拷贝。
- 右值传入时,移动。
- 字符串字面量时,直接在集合中构造,无额外临时。
重载引发的问题
为了支持通过索引传递名字,增加重载:
// 这是第一个版本,接收通用(万能)引用的模板函数
template<typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
// 这是第二个版本,专门接受 int 类型参数
void logAndAdd(int idx) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
问题:万能引用重载“贪婪”匹配
short nameIdx = 22;
logAndAdd(nameIdx); // 竟调用了万能引用版本,导致编译错误
-
虽然
short可以隐式转换成int,看起来应该调用logAndAdd(int),但是编译器却选择调用了模板函数版本。 -
原因是:
-
模板函数的万能引用可以“完美匹配”传入的任何类型,不需要类型转换。
-
普通函数需要进行类型提升(
short到int),属于“非精确匹配”。 -
C++ 重载解析中,精确匹配优先于类型转换匹配,所以模板函数胜出。
-
-
这导致了意料之外的行为,调用了模板版本,结果模板里代码可能并不支持
short类型,从而编译失败。 -
万能引用模板函数很“贪婪”,几乎可以匹配任何实参类型,往往比普通函数的重载匹配优先级更高,容易导致重载选择不符合预期,甚至引起编译错误。
完美转发构造函数的隐患
class Person {
public:
// 通用引用(万能引用)构造函数模板,完美转发参数初始化成员name
// 能接受几乎任何类型的参数,并将其转发给std::string的构造函数
template<typename T>
explicit Person(T&& n)
: name(std::forward<T>(n)) {}
// 接受int类型索引的构造函数,根据索引通过nameFromIdx函数获取对应的名字
explicit Person(int idx)
: name(nameFromIdx(idx)) {}
private:
std::string name; // 人名字符串成员
};
- 传入非
int的整型参数时,万能引用构造函数被调用。 - 更严重:拷贝构造可能被万能引用模板“劫持”,导致编译错误。
示例:
Person p("Nancy");
auto cloneOfP(p); // 编译错误,不调用拷贝构造,而是调用模板构造函数
原因:
- 模板实例化为
Person(Person&),匹配更优。 - 拷贝构造参数是
const Person&,匹配稍弱。 - 非
const左值调用完美转发构造优先。
结论
- 万能引用函数/构造函数非常贪婪,会匹配更多类型,导致重载解析出现意料之外的问题。
- 避免对万能引用形参的函数进行重载。
- 需要特殊处理时,使用其他设计技巧。

浙公网安备 33010602011771号