IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Item 26: Avoid overloading on universal references. -> 正文阅读

[C++知识库]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 类型的参数,完美转发构造函数会实例化出精确匹配的版本,最后代码将无法编译通过。

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

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 15:59:31  更:2022-04-06 16:01:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:42:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码