条款26:避免在万能引用上重载
万能引用重载易导致推导冲突,推荐用模板特化或SFINAE替代。
条款26:避免在万能引用上重载
条款26:避免在万能引用上重载
背景示例
假设有个函数,需要用名字打印日志并加入全局集合:
1
2
3
4
5
6
7
8
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);
}
调用:
1
2
3
4
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)
又调用拷贝构造拷贝进集合。
- 这里传入的是字符串字面量(
优化:用万能引用+完美转发
1
2
3
4
5
6
template<typename T>
void logAndAdd(T&& name) {
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
- 左值传入时,拷贝。
- 右值传入时,移动。
- 字符串字面量时,直接在集合中构造,无额外临时。
重载引发的问题
为了支持通过索引传递名字,增加重载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这是第一个版本,接收通用(万能)引用的模板函数
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));
}
问题:万能引用重载“贪婪”匹配
1
2
short nameIdx = 22;
logAndAdd(nameIdx); // 竟调用了万能引用版本,导致编译错误
虽然
short
可以隐式转换成int
,看起来应该调用logAndAdd(int)
,但是编译器却选择调用了模板函数版本。原因是:
模板函数的万能引用可以“完美匹配”传入的任何类型,不需要类型转换。
普通函数需要进行类型提升(
short
到int
),属于“非精确匹配”。C++ 重载解析中,精确匹配优先于类型转换匹配,所以模板函数胜出。
这导致了意料之外的行为,调用了模板版本,结果模板里代码可能并不支持
short
类型,从而编译失败。万能引用模板函数很“贪婪”,几乎可以匹配任何实参类型,往往比普通函数的重载匹配优先级更高,容易导致重载选择不符合预期,甚至引起编译错误。
完美转发构造函数的隐患
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
的整型参数时,万能引用构造函数被调用。 - 更严重:拷贝构造可能被万能引用模板“劫持”,导致编译错误。
示例:
1
2
Person p("Nancy");
auto cloneOfP(p); // 编译错误,不调用拷贝构造,而是调用模板构造函数
原因:
- 模板实例化为
Person(Person&)
,匹配更优。 - 拷贝构造参数是
const Person&
,匹配稍弱。 - 非
const
左值调用完美转发构造优先。
结论
- 万能引用函数/构造函数非常贪婪,会匹配更多类型,导致重载解析出现意料之外的问题。
- 避免对万能引用形参的函数进行重载。
- 需要特殊处理时,使用其他设计技巧。
本文由作者按照 CC BY 4.0 进行授权