文章

条款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 绑定给参数 nameconst 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),但是编译器却选择调用了模板函数版本。

  • 原因是:

    • 模板函数的万能引用可以“完美匹配”传入的任何类型,不需要类型转换。

    • 普通函数需要进行类型提升(shortint),属于“非精确匹配”。

    • 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 进行授权