Item 26: Avoid overloading on universal references.

Item 26: Avoid overloading on universal references.

Effective Modern C++ Item 26 的学习和解读。

这一节给出的建议是尽量不要对万能引用参数的函数进行重载,根因是重载函数的匹配规则。先从一个例子说起:

std::multiset<std::string> names;  // global data structure
void logAndAdd(const std::string& name)
{
  auto now = std::chrono::system_clock::now();  // get current time
  log(now, "logAndAdd");  // make log entry
  names.emplace(name);    // add name to global data structure; see Item 42 for info on emplace
}

上面的代码,我们看3个调用:

std::string petName("Darla");
logAndAdd(petName); // pass lvalue std::string
logAndAdd(std::string("Persephone")); // pass rvalue std::string
logAndAdd("Patty Dog"); // pass string literal

第一个调用:logAndAdd 的参数 name 被绑定到一个左值 petName 上。由于 name 是一个左值,names.emplace(name) 将发生一次拷贝。

第二个调用:std::string(“Persephone”) 首先会显示构造出一个临时的 std::string,并且是一个右值。name 被绑定到一个右值,但是 name 是一个左值,names.emplace(name) 将发生一次拷贝。

第三个调用:“Patty Dog” 传入 logAndAdd 将隐式构造出一个临时的 std::string,并且是一个右值。name 被绑定到一个右值,但是 name 是一个左值,names.emplace(name) 将发生一次拷贝。

后面两个调用点, name 都是绑定到一个右值,我们可以通过移动来代替拷贝来提高性能,我们很容易使用万能引用重写 logAndAdd 如下:

template<typename T>
void logAndAdd(T&& name)
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(std::forward<T>(name));
}

std::string petName("Darla");
logAndAdd(petName);  // as before, copy lvalue into multiset
logAndAdd(std::string("Persephone")); // move rvalue instead of copying it
logAndAdd("Patty Dog"); // create std::string in multiset instead of copying a temporary std::string

现在,步入本节的主题。对于上述代码,假设在 logAndAdd 内部需要根据一个索引查找 name,logAndAdd 被重载成这样:

std::string nameFromIdx(int idx); // return name corresponding to idx
void logAndAdd(int idx) // new overload
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(nameFromIdx(idx));
}

template<typename T>  // as berfore
void logAndAdd(T&& name)
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(std::forward<T>(name));
}

新增一个 int 类型参数的调用方式:

std::string petName("Darla");
logAndAdd(petName);                     // as before, these
logAndAdd(std::string("Persephone"));   // calls all invoke
logAndAdd("Patty Dog");                 // the T&& overload

logAndAdd(22); // calls int overload

以上还没什么问题,一切都还符合我们的预期。但是,考略下面的调用场景:

short nameIdx;// give nameIdx a value
logAndAdd(nameIdx);  // error!

对于 short 类型的 nameIdx,我们期望的显示是调用 int 类型的 logAndAdd 重载。但事实却是这样:万能引用版本的 T 将被推导成 short,因而产生一个确切的匹配版本,然后在 names.emplace 时候会用 short 类型去构造 std::string,显然会报错。

在 C++ 中,以万能引用为参数的函数是最贪婪的函数,它能实例化出多数能够胜任的精确匹配版本,而这个例子中 short 需要做类型转换成 int 类型才会匹配到 int 类型的 logAndAdd。而 C++ 重载函数的匹配原则:如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本。因此这里会匹配到万能引用实例化出的版本。

再看万能引用构造函数的例子:

class Person {
public:
  template<typename T>
  explicit Person(T&& n)         // perfect forwarding ctor;
  : name(std::forward<T>(n)) {}  // initializes data member
  explicit Person(int idx)       // int ctor
  : name(nameFromIdx(idx)) {}private:
  std::string name;
};

这里会有两个问题。首先,传一个除 int 外的整形类型(比如,std::size_t, short, long)将不会调用 int 版本的构造函数,而是调用万能l引用版本的构造函数,然后这将导致编译失败。然后还有一个更加糟糕的问题,根据 Item 17: Understand special member function generation. 介绍我们知道编译器将在合适的条件下生成 copy 和 move 类构造函数。Person 实际可能是下面这个样子:

class Person {
public:
  template<typename T>  // perfect forwarding ctor
  explicit Person(T&& n)
  : name(std::forward<T>(n)) {}
  explicit Person(int idx);   // int ctor
  
  Person(const Person& rhs);  // copy ctor (compiler-generated)
  Person(Person&& rhs);  // move ctor (compiler-generated)};

考虑下面的调用:

Person p("Nancy");
auto cloneOfP(p); // create new Person from p; this won't compile!

使用 p 去创建一个新的 Person,这里不会调用 Person 的拷贝构造函数,而会调用完美转发构造函数。这是因为 Person 的拷贝构造函数的参数是一个 const 类型的 ,而 p 是一个非 const 类型,并且完美转发构造函数会实例化出一个精确的匹配版本。当我们稍微改造下 p,就可以调用编译器生成的拷贝构造函数:

const Person cp("Nancy");  // object is now const
auto cloneOfP(cp);         // calls copy constructor!

虽然完美转发构造函数也能实例化出一个精确函数签名的版本,但是 C++ 重载匹配会选择普通的重载版本。

当继承介入进来之后,问题将变得更加让人无法接受,我们看下这样的代码片段:

class SpecialPerson: public Person {
public:
  SpecialPerson(const SpecialPerson& rhs) // copy ctor; calls
  : Person(rhs)                           // base class
  {}                                   // forwarding ctor!
  SpecialPerson(SpecialPerson&& rhs)      // move ctor; calls
  : Person(std::move(rhs))                // base class
  {}                                   // forwarding ctor!
};

这两个构造函数都会调用基类的完美转发构造函数,因为这两个构造函数给基类传入的都是 SpecialPerson 类型的参数,完美转发构造函数会实例化出精确匹配的版本,最后代码将无法编译通过。

总之,对万能引用参数函数进行重载是一个糟糕的设计,我们需要尽量避免。

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值