Modern C++ 最核心的变化是什么?

点击蓝字

d0e913795f17a990c6f0b8ef2b6eb3e6.png

关注我们

因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享

来源于网络,侵删

个人觉得最核心的变化是右值引用的引入,右值引用是  C++ 走向现代化的最重要一步。建议每一位 C++ 开发者都应该深入去了解并充分使用它。

右值引用是 C++11 中最重要的新特性之一,它解决了 C++ 中大量的历史遗留问题,使 C++ 标准库的实现在多种场景下消除了不必要的额外开销(如 std::vectorstd::string),也使得另外一些标准库(如 std::unique_ptrstd::function)成为可能。即使你并不直接使用右值引用,也可以通过标准库,间接从这一新特性中受益。为了更好地理解标准库结合右值引用带来的优化,我们有必要了解一下右值引用的重大意义。

右值引用的意义通常解释为两大作用:移动语义和完美转发。本文主要讨论移动语义。

一、移动语义

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在 C++11 之前,移动语义的缺失是 C++ 饱受诟病的问题之一。

举个例子。

问题一:如何将大象放入冰箱?

答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。

问题二:如何将大象从一台冰箱转移到另一台冰箱?

普通解答:

打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。

2B 解答:

在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。

等等,这个 2B 解答听起来很耳熟,这不就是 C++ 中要移动一个对象时所做的事情吗?

“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++ 通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。

为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++ 的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。

右值(相对应的还有左值)是从 C 语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++ 中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是 static_cast<T&&>() 的简单封装。

右值引用至少可以解决以下场景中的移动语义缺失问题:

1.按值传入参数

按值传参是最符合人类思维的方式。基本的思路是,如果传入参数是为了将资源交给函数接受者,就应该按值传参。同时,按值传参可以兼容任何的 cv-qualified 左值、右值,是兼容性最好的方式。

class People {
public:// 按值传入字符串,可接收左值、右值。// 接收左值时为复制,接收右值时为移动People(string name) : name_(move(name)) // 显式移动构造,将传入的字符串移入成员变量{}string name_;
};People a("Alice"); // 移动构造namestring bn = "Bob";
People b(bn); // 拷贝构造name

构造a时,调用了一次字符串的构造函数和一次字符串的移动构造函数。如果使用 const string& name 接收参数,那么会有一次构造函数和一次拷贝构造,以及一次 non-trivial 的析构。尽管看起来很蛋疼,尽管编译器还有优化,但从语义来说按值传入参数是最优的方式。

如果你要在构造函数中接收 std::shared_ptr<X> 并且存入类的成员(这是非常常见的),那么按值传入更是不二选择。拷贝 std::shared_ptr<X> 需要线程同步,相比之下移动 std::shared_ptr 是非常轻松愉快的。

2.按值返回

和接收输入参数一样,返回值按值返回也是最符合人类思维的方式。曾经有无数函数为了返回容器而不得不写成这样:

// 一个按值语义定义的字符串拆分函数
void str_split(const string& s, vector<string>* vec);

这里不考虑分隔符,假定分隔符是固定的。这样要求 vec在外部被事先构造,此时尚无从得知vec的大小。即使函数内部有办法预测vec的大小,因为函数并不负责构造vec,很可能仍需要 resize`。

对这样的函数嵌套调用更是痛苦的事情,谁用谁知道啊。

有了移动语义,就可以写成这样:

vector<string> str_split(const string& s) {vector<string> v;// ...return v; // v是左值,但优先移动,不支持移动时仍可复制。
}

如果函数按值返回,return 语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求优先调用移动构造函数,如果不符再调用拷贝构造函数。尽管 ``v是左值,仍然会优先采用移动语义,返回 vector` 从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。

对于 std::unique_ptr 来说,这简直就是福音。

unique_ptr<SomeObj> create_obj(/*...*/) {unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));ptr->foo(); // 一些可能的初始化return ptr;
}

当然还有更简单的形式:

unique_ptr<SomeObj> create_obj(/*...*/) {return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}

在工厂类中,这样的语义是非常常见的。返回 unique_ptr 能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回栈上的左值和右值,但都适用移动语义(unique_ptr 不支持拷贝)。

3.接收右值表达式

没有移动语义时,以表达式的值(例为函数调用)初始化对象或者给对象赋值是这样的:

vector<string> str_split(const string& s);
// 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。
vector<string> v = str_split("1,2,3"); 
vector<string> v2;
// 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。
v2 = str_split("1,2,3");

注:v 的拷贝构造调用有可能被优化掉,尽管如此在语义上仍然是有一次拷贝操作。

同样的代码,在支持移动语义的世界里就变得更美好了。

vector<string> str_split(const string& s);
// 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。
vector<string> v = str_split("1,2,3"); 
vector<string> v2;
// 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。
v2 = str_split("1,2,3");

注:v 的移动构造调用有可能被优化掉,尽管如此在语义上仍然是有一次移动操作。

不用多说也知道上面的形式是多么常用和自然。而且这里完全没有任何对右值引用的显式使用,性能提升却默默的实现了。

4.对象存入容器

这个问题和前面的构造函数传参是类似的。不同的是这里是按两种引用分别传参。参见 std::vector 的 push_back 函数。

void push_back( const T& value ); // (1)
void push_back( T&& value ); // (2)

不用多说自然是左值调用 1 右值调用 2。如果你要往容器内放入超大对象,那么版本 2 自然是不 2 选择。

vector<vector<string>> vv;vector<string> v = {"123", "456"};
v.push_back("789"); // 临时构造的string类型右值被移动进容器v
vv.push_back(move(v)); // 显式将v移动进vv

困扰多年的难言之隐是不是一洗了之了?

5.std::vector 的增长

又一个隐蔽的优化。当 vector 的存储容量需要增长时,通常会重新申请一块内存,并把原来的内容一个个复制过去并删除。对,复制并删除,改用移动就够了。

对于像 vector<string> 这样的容器,如果频繁插入造成存储容量不可避免的增长时,移动语义可以带来悄无声息而且美好的优化。

6.std::unique_ptr放入容器

曾经,由于 vector 增长时会复制对象,像 std::unique_ptr 这样不可复制的对象是无法放入容器的。但实际上 vector 并不复制对象,而只是“移动”对象。所以随着移动语义的引入,std::unique_ptr 放入 std::vector 成为理所当然的事情。

容器中存储 std::unique_ptr 有太多好处。想必每个人都写过这样的代码:

MyObj::MyObj() {for (...) {vec.push_back(new T());}// ...
}MyObj::~MyObj() {for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {if (*iter) delete *iter;}// ...
}

繁琐暂且不说,异常安全也是大问题。使用 vector<unique_ptr<T>>,完全无需显式析构,unqiue_ptr 自会打理一切。完全不用写析构函数的感觉,你造吗?

unique_ptr 是非常轻量的封装,存储空间等价于裸指针,但安全性强了一个世纪。实际中需要共享所有权的对象(指针)是比较少的,但需要转移所有权是非常常见的情况。auto_ptr 的失败就在于其转移所有权的繁琐操作。unique_ptr 配合移动语义即可轻松解决所有权传递的问题。

注:如果真的需要共享所有权,那么基于引用计数的 shared_ptr 是一个好的选择。shared_ptr 同样可以移动。由于不需要线程同步,移动 shared_ptr 比复制更轻量。

7.std::thread 的传递

thread 也是一种典型的不可复制的资源,但可以通过移动来传递所有权。同样 std::future std::promise std::packaged_task 等等这一票多线程类都是不可复制的,也都可以用移动的方式传递。

二、完美转发

除了移动语义,右值引用还解决了 C++03 中引用语法无法转发右值的问题,实现了完美转发,才使得 std::function 能有一个优雅的实现。这部分不再展开了。

三、总结

移动语义绝不是语法糖,而是带来了 C++ 的深刻革新。移动语义不仅仅是针对库作者的,任何一个程序员都有必要去了解它。尽管你可能不会去主动为自己的类实现移动语义,但却时时刻刻都在享受移动语义带来的受益。因此这绝不意味着这是一个可有可无的东西。

所以,如果你要写出优雅的 Modern C++ 代码,应该多使用右值引用,喜欢它,拥抱它。

f93d24daacb9ba14978008ac35339a4f.gif

如果你年满18周岁以上,又觉得学【C语言】太难?想尝试其他编程语言,那么我推荐你学Python,现有价值499元Python零基础课程限时免费领取,限10个名额!
▲扫描二维码-免费领取

推荐阅读

OPPO 推出“安第斯智能云”,三大技术护城河已形成

“告别 8 万行 C++ 代码,我用 4 千行 C 代码就搞定了!”

这才是B站的正确打开方式!

Intellij IDEA 神级插件!效率提升 10 倍!

点击 阅读原文 了解更多

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/336937.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式示例_命令设计模式示例

设计模式示例本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#x…

第一个C语言编译器是怎样编写的

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删不知道你有没有想过&#xff0c;某种编程语言的第一个编译器是怎么来的呢&#xff1f;这不就是“鸡生蛋&#xff0c;蛋生鸡”的问题吗&#xff1…

备忘录模式 命令模式_备忘录设计模式示例

备忘录模式 命令模式本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 …

还在分析我写的 bug 啊,我都是随便写的!

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删还在分析我写的 bug 啊&#xff0c;我都是随便写的&#xff01;客户就是可以为所欲为&#xff0c;怎么地&#xff01;下面教大家如何对付产品经理…

mysql如何让表建立连接吗_MySQL 表与表之间建立关系

引子&#xff1a;如下图是一张非常寻常的表格&#xff0c;在以前的工作中实常会制作类似的表格&#xff0c;但是今天的数据库内容&#xff0c;将我之前这种传统的制表思路上升了一个层次&#xff1b;今天核心的内容就是怎样让表与表之间产生关系&#xff0c;在思考这个问题的时…

访客模式 无痕模式 区别_访客设计模式示例

访客模式 无痕模式 区别本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查…

项目经理升职了是啥_什么是升职率?

项目经理升职了是啥我确实相信您熟悉彼得原则 。 一般而言&#xff0c;该原则是一种观察&#xff0c;即晋升可能会并且将导致晋升人员不再符合工作资格的情况。 对于JVM&#xff0c;存在类似的问题。 太快地提升对象可能会对性能产生重大影响。 在这篇文章中&#xff0c;我们将…

mysql事件定时_MySQL事件(定时任务)

whats the MySQL 事件MySQL5.1 版本开始引进 event 概念&#xff0c;MySQL 中的事件(event&#xff1a;时间触发器)是用于执行定时或周期性的任务&#xff0c;类似 Linux 中的 crontab&#xff0c;事件可以精确到秒。通过单独或调用存储过程使用&#xff0c;在某一特定的时间点…

lucene简介_Lucene简介

lucene简介本文是我们名为“ Apache Lucene基础知识 ”的学院课程的一部分。 在本课程中&#xff0c;您将了解Lucene。 您将了解为什么这样的库很重要&#xff0c;然后了解Lucene中搜索的工作方式。 此外&#xff0c;您将学习如何将Lucene Search集成到您自己的应用程序中&…

jsp mysql 插入数据_jsp连接MySQL实现插入insert操作功能示例

下午终于实现了jsp连接MySQL执行插入操作的功能。在index.jsp页面输入数据&#xff0c;提交到mysql——insert.jsp页面进行插入数据库的操作。index.jsp页面代码如下&#xff1a;request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8&…

swagger生成示例_生成器设计模式示例

swagger生成示例本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &…

设计模式适配器模式_适配器设计模式示例

设计模式适配器模式本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &…

mysql mgr bug_Mysql MGR架构误操作引发的问题处理

【背景介绍】故障方描述&#xff1a;一次用户刷权限的时候不小心把数据库用户表记录删掉了&#xff0c;执行之后发现不对后重建用户&#xff0c;杀掉进程后重新MGR启动报错。【报错信息】2018-06-13T12:47:41.405593Z 32 [Note] Plugin group_replication reported: Group comm…

css外墙法_外墙设计模式示例

css外墙法本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#xff0…

设计模式示例_桥梁设计模式示例

设计模式示例本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#x…

数值分析 使用c语言 源码_分析源码,学会正确使用 Java 线程池

在日常的开发工作当中&#xff0c;线程池往往承载着一个应用中最重要的业务逻辑&#xff0c;因此我们有必要更多地去关注线程池的执行情况&#xff0c;包括异常的处理和分析等。本文主要聚焦在如何正确使用线程池上&#xff0c;以及提供一些实用的建议。文中会稍微涉及到一些线…

设计模式示例_状态设计模式示例

设计模式示例本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#x…

cam350 不能打开光绘文件_如何在CAM350中导入Allegro光绘

内容简介&#xff1a;本人之前遇到过 CAM350 导入 Allegro 格式光绘后钻孔偏移、异型孔不显示的问题&#xff0c;捣鼓过之后找到一个方法&#xff0c;在 Allegro 群友的建议下制作该文档进行适当的说明。1. 导入 Gerber 文件鉴于手工导入 Gerber 文件需要逐层设置&#xff0c;过…

d3js mysql_D3.js入门指南

近期略有点诸事不顺&#xff0c;趁略有闲余之时&#xff0c;玩起D3.js。之前实际项目中主要是用各种chart如hightchart、echarts等&#xff0c;这些图形库玩起来貌都是完美的&#xff0c;一切皆可配置&#xff0c;但几年前接触了D3之后&#xff0c;觉得前面那chart类库局限的地…

layui templet格式化_layui使用templet格式化表格数据的方法

增加js/*---------------------格式化时间开始--------------------------*///对Date的扩展&#xff0c;将 Date 转化为指定格式的String//月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符&#xff0c;//年(y)可以用 1-4 个占位符&#xff0c;毫秒(S)只能用…