引转移——避免在通用引用上重载
文章目录
- 避免在通用引用上重载
- 最危险的反模式
- 为什么通用引用重载如此危险?
- 特殊危险地带:构造函数
- 何时"安全"?
避免在通用引用上重载
| 核心要点 | 要点 |
|---|---|
| 1 | 在通用引用上重载,几乎总是导致函数被意外调用的频率远超预期 |
| 2 | 完美转发构造函数尤其危险:会劫持非 const 左值的拷贝请求,并劫持派生类对基类拷贝/移动构造的调用 |
| 3 | 通用引用的"贪婪匹配"特性使其在重载决议中具有压倒性优势 |
| 4 | 替代方案见条款27——不要试图在通用引用上重载 |
最危险的反模式
//全局容器std::multiset<std::string>names;// 版本1:针对左值voidlogAndAdd(conststd::string&name){names.insert(name);}// 版本2:为"高效"处理右值而添加的通用引用重载 ❌template<typenameT>voidlogAndAdd(T&&name){names.insert(std::forward<T>(name));}看起来完美——直到你真正使用它:
std::string name="Alice";logAndAdd(name);// 调用版本1 还是版本2?// 答案:调用版本2!T 推导为 std::string&// 模板版本是精确匹配,压倒一切// 如果 name 是 const 的,版本1 仍然不匹配——还是版本2shortidx=42;logAndAdd(idx);// ❌ 编译错误!// T 推导为 short,std::multiset<std::string>::insert 期望 std::string// short 本可隐式转换为 std::string(若调用版本1),但模板完美匹配 short// 错误信息埋藏在 std::string 构造函数深处 —— 令人发指!为什么通用引用重载如此危险?
📌核心矛盾:通用引用模板是 C++ 的"贪婪匹配器"——它能精确匹配几乎任何类型,远比需要类型转换的非模板重载版本更有竞争力。
重载决议优先级: 1. 精确匹配(通用引用 T&&) ← 🏆 被选中! 2. 需要派生类 → 基类转换 3. 需要 const 转换 4. 需要用户定义的隐式转换 ... 99. 需要标准隐式类型转换(const T&) ← 😢 被跳过特殊危险地带:构造函数
classPerson{public:template<typenameT>explicitPerson(T&&n)// 通用引用构造("贪婪"):name(std::forward<T>(n)){}Person(constPerson&rhs);// 拷贝构造(编译器生成或自定义)private:std::string name;};Personp("Nancy");autocloneOfP(p);// ❌ 编译错误!// 你期望:调用 Person(const Person&) 拷贝构造函数// 实际发生:调用 Person(T&&) 通用引用版本!//// T 推导为 Person& → Person(Person& n)// p 不是 const → 精确匹配 Person& 比 const Person& 更优!classSpecialPerson:publicPerson{public:SpecialPerson(constSpecialPerson&rhs):Person(rhs)// ❌ 调用 Person(T&&),而非 Person(const Person&)!{}SpecialPerson(SpecialPerson&&rhs):Person(std::move(rhs))// ❌ 同样问题{}};// rhs 和 std::move(rhs) 的类型是 SpecialPerson& 和 SpecialPerson&&// 而非 Person& / Person&& → 模板比基类拷贝/移动更匹配!何时"安全"?
🔧只有当通用引用是唯一的重载版本,或者模板参数受到足够严格的约束,通用引用重载才可能安全。
// ✅ 安全:唯一版本,没有重载template<typenameT>voidlogAndAdd(T&&name){names.insert(std::forward<T>(name));}