条款 26 - 避免依万能引用型别进行重载
这一讲开门见山: 将一个接受万能引用参数的函数或模板与其他函数进行重载,是一个非常危险的设计,因为它几乎总是会导致意想不到的行为和难以理解的编译错误。
核心问题:万能引用的“贪婪”
万能引用(在模板中声明为 T&&)之所以危险,是因为它过于“贪婪”。根据C++的模板类型推导和重载决议规则,万能引用可以为几乎任何类型的实参生成一个精确匹配 (Exact Match) 的函数实例。
在重载决议中,精确匹配的优先级非常高,高于那些需要类型提升(如 short 到 int)、类型转换(如派生类到基类)或添加 const 的匹配。这意味着,一旦万能引用版本的重载成为候选,它就会像磁铁一样“吸走”大量本意是想调用其他重载版本的函数调用。
示例一:logAndAdd —— 万能引用与常规函数的重载
这个例子展示了最直接的问题。假设我们有一个高效的、使用万能引用的 logAndAdd 函数,同时为了方便,我们又重载了一个接受 int 索引的版本, 当我们用一个 short 类型的变量来调用它时:
1 | // 全局数据 |
对于尝试匹配 logAndAdd(int):short 到 int 的转换是类型提升 (Promotion),这是一个合法的匹配,但不是精确匹配。
而对于匹配 logAndAdd(T&&):编译器可以为万能引用实例化一个版本,其中 T 被推导为 short&。这个 logAndAdd(short&) 的实例对于 short 类型的实参来说是精确匹配 (Exact Match)。
由于精确匹配的优先级高于类型提升,编译器选择了万能引用版本的
logAndAdd。也就是说, 在万能引用版本的函数体内,代码尝试用一个 short
去构造 names(一个 std::multiset
示例二:Person 构造函数 —— 完美转发构造函数的危险
这个例子揭示了一个更微妙、更危险的问题。当一个类的构造函数被重载为万能引用时(通常被称为“完美转发构造函数”),它会“劫持”类的拷贝和移动机制。
1 | class Person { |
- 匹配拷贝构造函数:实参 p 的类型是 Person(一个非 const 左值)。为了匹配拷贝构造函数的参数 const Person&,需要为 p 添加 const 限定符。这是一个合法的匹配,但不是精确匹配。
- 匹配完美转发构造函数:编译器可以为万能引用构造函数实例化一个版本,其中 T 被推导为 Person&。这个 Person(Person&) 的实例对于 Person 类型的左值实参 p 来说是精确匹配。
同样,精确匹配胜出。编译器选择了完美转发构造函数,而不是拷贝构造函数。在完美转发构造函数体内,代码尝试用 p(一个 Person 对象)来初始化 name(一个 std::string 成员)。这当然会失败,并产生一堆令人费解的错误信息。
在这个例子里, 完美转发构造函数破坏了类的基本功能(拷贝)。它同样会劫持派生类对基类的拷贝和移动调用,导致继承体系出现问题。
总而言之, 在设计函数和类时,应极力避免将接受万能引用的版本与其他版本进行重载。在下一讲条款27会详细介绍如何处理那些确实需要类似重载行为的场景,例如使用标签分派、static_assert 或 C++20 的 concepts 等技术来约束模板,从而安全地实现期望的功能。