左值引用的限制

2024-03-19 03:32
文章标签 引用 限制 左值

本文主要是介绍左值引用的限制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

C++0x, rvalue reference, move semantics, RVO, NRVO — 我们到底要什么 收藏 此文于2010-05-25被推荐到CSDN首页 如何被推荐? Visual C++ 2010 (VC10) 实现了一些颇有用处的 C++0x 新特性,其中就包括(万众期待的)rvalue reference 。 本文不打算详述 rvalue reference 是什么了,关于这方面的文章已经不少,读者可以自己搜索来看看。我要说的是,今天我做了一些非常简单的关于 rvalue reference 的性能测试,其中有非常鼓舞人心的部分,也有 C++ 一以贯之的复杂和越来越复杂的部分。 好消息:性能的极大提升 从原理上讲,rvalue reference 使得 move semantics 成为可能,从而让编译器可以从rvalue对象中“偷走”资源,而不是拷贝数据,在很多情况下,这会带来性能的极大提升。 测试代码很简单:比较 copy 和 move 一个 vector 对象的时间: view plaincopy to clipboardprint? 1. #include 2. #include 3. #include 4. #include 5. using namespace std; 6. vector make_vector() 7. { 8. vector v(1, string("this is a string")); 9. return v; 10. } 11. int main() 12. { 13. vector src(make_vector()), s(make_vector()); 14. clock_t start, end; 15. start = clock(); 16. for (int i = 0; i < 1000000; ++i) 17. { 18. vector s = src; 19. } 20. end = clock(); 21. cout << "Vector copy ctor takes: " << end - start << endl; 22. start = clock(); 23. for (int i = 0; i < 1000000; ++i) 24. { 25. vector s = move(src); 26. } 27. end = clock(); 28. cout << "Vector move ctor takes: " << end - start << endl; 29. return 0; 30. } #include #include #include #include using namespace std; vector make_vector() { vector v(1, string("this is a string")); return v; } int main() { vector src(make_vector()), s(make_vector()); clock_t start, end; start = clock(); for (int i = 0; i < 1000000; ++i) { vector s = src; } end = clock(); cout << "Vector copy ctor takes: " << end - start << endl; start = clock(); for (int i = 0; i < 1000000; ++i) { vector s = move(src); } end = clock(); cout << "Vector move ctor takes: " << end - start << endl; return 0; } 在我的相当老旧的笔记本上,Release 版本的输入是这样的 Vector copy ctor takes: 4562 Vector move ctor takes: 4 看来相当鼓舞人心不是?对于一个还不是太大太复杂的对象,move 比 copy 竟然能有千倍的性能提高!如果把 vector 的尺寸加大,move 版本的执行时间并不会有多大区别,而 copy 版本的执行时间则会随对象增大而延长。我们以后写一个函数来构造对象时,再也不需要使用丑陋的类似 view plaincopy to clipboardprint? 1. void make_vector(vector& out) void make_vector(vector& out) 之类的办法来避免对象拷贝,我们只需要在返回点或者调用点加上 move ! 好吧,如果够细心,你会发现上面的代码玩了个花招:它没有在循环中调用 make_vector ,它只是把值保存起来,然后采用 copy 和 move 。第一个原因是如果在循环中调用 make_vector ,我们测得的大多数时间就都在构造上了,copy 和 move 之间的区别无法显示出来;第二个原因,后面会谈到。 如果你一定要看看在循环中调用 make_vector 的结果,也就是说把测试代码中的 vector s = src; 和 vector s = move(src); 分别替换为 vector s = make_vector(); 和 vector s = move(make_vector()); 在我这里运行结果是这样的 Vector copy ctor takes: 7928 Vector move ctor takes: 3587 很明显,两个循环多执行的时间大致相同,那就是构造对象的时间了。 在 C++ 里,凡事都有例外 如果我们把测试对象换成 string,在 string 的大小比较大的时候,结果大体相似,例如下面的程序测试 copy 和 move 大小为 20 的 string: view plaincopy to clipboardprint? 1. #include 2. #include 3. #include 4. using namespace std; 5. int main() 6. { 7. string src(20, 'e'); 8. clock_t start, end; 9. start = clock(); 10. for (int i = 0; i < 1000000; ++i) 11. { 12. string s = src; 13. } 14. end = clock(); 15. cout << "String copy ctor takes: " << end - start << endl; 16. start = clock(); 17. for (int i = 0; i < 1000000; ++i) 18. { 19. string s = move(src); 20. } 21. end = clock(); 22. cout << "String move ctor takes: " << end - start << endl; 23. return 0; 24. } #include #include #include using namespace std; int main() { string src(20, 'e'); clock_t start, end; start = clock(); for (int i = 0; i < 1000000; ++i) { string s = src; } end = clock(); cout << "String copy ctor takes: " << end - start << endl; start = clock(); for (int i = 0; i < 1000000; ++i) { string s = move(src); } end = clock(); cout << "String move ctor takes: " << end - start << endl; return 0; } 在我这里,输出差不多是 String copy ctor takes: 1728 String move ctor takes: 40 由于拷贝 string 是一个比较快的操作,所以差距没有那么大,但仍然相当明显。 到这里,你一定会说“好,从此我一定会在代码里让 rvalue reference 和 move 满天飞” 然而,如果你把 string src 的尺寸缩小一点,到达15的时候,情况变了,输出差不多是 String copy ctor takes: 40 String move ctor takes: 42 为什么拷贝一个15个字符的 string 比拷贝20个字符快那么多?读读 string 类就会发现,string 类会给自己预分配16字节的 buffer,如果拷贝对象不超过15个字符,就不需要重新分配空间,只需要调用 memcpy 就可以,这是一个相当高效的操作。而 move 在这种情况下则选择不进行指针交换,而是调用 memmove,这往往比 memcpy 要慢一些。 我们得出什么结论呢?有几个 1. 拷贝,尤其是少量数据的拷贝,其实很高效 2. 动态内存分配相当昂贵,从上面的结果可以大致推断出,分配一片空间大概比拷贝20个字节多花40倍的时间 3. 小字符串(15个字符以下)的拷贝已经足够优化了 我还没有打算到此打住,如果就这么简单,那就不是 C++ 了。如果仔细考察对象的 copy 和 move ,事情会更加复杂。 返回值和 RVO 写一个很简单的类 Foo,它的作用是帮我们了解 copy 和 move 之间,到底发生了什么事。 view plaincopy to clipboardprint? 1. #include 2. using namespace std; 3. struct Foo 4. { 5. Foo() { cout << "Foo ctor" << endl; } 6. Foo(const Foo&) { cout << "Foo copy ctor" << endl; } 7. void operator=(const Foo&) { cout << "Foo operator=" << endl; } 8. Foo(Foo&&) { cout << "Foo move ctor" << endl; } 9. ~Foo() { cout << "Foo dtor" << endl; } 10. void bar() {} 11. }; 12. Foo make_foo() 13. { 14. return Foo(); 15. } 16. int main() 17. { 18. cout << "Copy from rvalue: " << endl; 19. Foo f1 = make_foo(); 20. cout << "-----------------------" << endl; 21. cout << "Move from rvalue: " << endl; 22. Foo f2 = move(make_foo()); 23. cout << "-----------------------" << endl; 24. return 0; 25. } #include using namespace std; struct Foo { Foo() { cout << "Foo ctor" << endl; } Foo(const Foo&) { cout << "Foo copy ctor" << endl; } void operator=(const Foo&) { cout << "Foo operator=" << endl; } Foo(Foo&&) { cout << "Foo move ctor" << endl; } ~Foo() { cout << "Foo dtor" << endl; } void bar() {} }; Foo make_foo() { return Foo(); } int main() { cout << "Copy from rvalue: " << endl; Foo f1 = make_foo(); cout << "-----------------------" << endl; cout << "Move from rvalue: " << endl; Foo f2 = move(make_foo()); cout << "-----------------------" << endl; return 0; } 输出是什么呢? Copy from rvalue: Foo ctor ----------------------- Move from rvalue: Foo ctor Foo move ctor Foo dtor ----------------------- Foo dtor Foo dtor 怎么回事?当我们 copy 的时候,仅仅只调用了一个 constructor,甚至没有调用 copy constructor ,而我们 move 的时候,却需要调用一个 constructor,一个 move constructor 和一个 destructor。 Move 的情况比较容易理解,分为三步: 1. 调用 constructor 构造一个临时对象 2. 从这个临时对象进行 move constructing 3. 销毁这个临时对象 而 copy 为什么这么省事?因为编译器会使用 RVO(return value optimization),在返回值是一个 rvalue 的时候,这个对象会直接构造在接收返回值的对象空间中,从而减少了拷贝。而相反 move 则会阻碍编译器进行 RVO,反而增加了两个函数调用,如果 destructor 涉及动态空间的释放以及一些耗时的操作,那可是偷鸡不成蚀把米。 那我们又得到什么结论呢? 1. RVO 是个好东西 2. 在有 RVO 的时候,move semantics 未必比较快 我还没有打算住手,好戏在后面: NRVO 如果把函数 make_foo 改成这个模样: view plaincopy to clipboardprint? 1. Foo make_foo() 2. { 3. Foo f; 4. return f; 5. } Foo make_foo() { Foo f; return f; } 在 Debug 模式运行一下,结果就更加有趣了: Copy from rvalue: Foo ctor Foo move ctor Foo dtor ----------------------- Move from rvalue: Foo ctor Foo move ctor Foo dtor Foo move ctor Foo dtor ----------------------- Foo dtor Foo dtor 为什么?为什么我们明明打算 copy ,却调到了 move constructor;而 move 的时候,却调用了两个 move constructor ?我们一条条的分析。 copy 首先,无论 copy 还是 move,函数 make_foo 中 的 Foo f 都会导致一个 constructor 。 在这里 f 是一个 lvalue,所以在 copy 时,编译器没法对它进行 RVO,而在 Debug 模式下其它的优化又关掉了,于是只好用返回值构造对象 f1。 这里有新东西出现了:新的 C++ 标准要求,在构造返回的临时对象时,如果不使用 RVO,而类定义了 move constructor,优先使用 move constructor。所以我们看到的 move constructor 调用,是用来初始化临时对象的。 而有了这个临时对象,编译器倒是可以直接把它扔给 f1,从而节省一道从临时对象到 f1 的拷贝。 之后局部对象 f 超出作用范围,被销毁。 move 构造对象 f。 和 copy 一样,用 move constructor 构造临时对象。 这里问题来了:加入 move() 调用使得编译器无法优化掉临时对象到 f2 的拷贝,于是编译器退而求其次,用 move constructor 来初始化 f2。 局部对象 f 被销毁。 临时对象被销毁。 好的,我们看到 move() 又杯具了,如果是 Release 模式,会如何呢?结果是这样的: Copy from rvalue: Foo ctor ----------------------- Move from rvalue: Foo ctor Foo move ctor Foo dtor ----------------------- Foo dtor Foo dtor 这里 copy 和 move 双方各减少了一个对象生成。是哪一个呢?答案是临时对象。这要归功于编译器的 NRVO (Named Return Value Optimization),这种优化让编译器能够在返回一个 lvalue 的情况下,也减少一个对象 copy(或 move),但是这并没能优化掉对于 f2 的构造。 结论 Rvalue reference, move semantics 都是好东西,std::move() 也是好东西,但是用得不对可能会适得其反。 事实上,在有了 move semantics 之后,最高效的的返回正是我们熟悉的形式: view plaincopy to clipboardprint? 1. Foo make_foo() 2. { 3. Foo f; 4. return f; 5. } 6. …… 7. Foo f1 = make_foo(); Foo make_foo() { Foo f; return f; } …… Foo f1 = make_foo(); 因为编译器会尽可能的使用 RVO 和 NRVO,而在无法使用这些优化时,由于 make_foo 返回一个 rvalue ,编译器仍会尽力调用 move constructor,而只有这些都失败了,编译器才会采取我们熟悉的 copy constructor --- 总之,不会比这个更坏了。

这篇关于左值引用的限制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/824644

相关文章

poj 2135 有流量限制的最小费用最大流

题意: 农场里有n块地,其中约翰的家在1号地,二n号地有个很大的仓库。 农场有M条道路(双向),道路i连接着ai号地和bi号地,长度为ci。 约翰希望按照从家里出发,经过若干块地后到达仓库,然后再返回家中的顺序带朋友参观。 如果要求往返不能经过同一条路两次,求参观路线总长度的最小值。 解析: 如果只考虑去或者回的情况,问题只不过是无向图中两点之间的最短路问题。 但是现在要去要回

poj 3422 有流量限制的最小费用流 反用求最大 + 拆点

题意: 给一个n*n(50 * 50) 的数字迷宫,从左上点开始走,走到右下点。 每次只能往右移一格,或者往下移一格。 每个格子,第一次到达时可以获得格子对应的数字作为奖励,再次到达则没有奖励。 问走k次这个迷宫,最大能获得多少奖励。 解析: 拆点,拿样例来说明: 3 2 1 2 3 0 2 1 1 4 2 3*3的数字迷宫,走两次最大能获得多少奖励。 将每个点拆成两个

poj 2195 bfs+有流量限制的最小费用流

题意: 给一张n * m(100 * 100)的图,图中” . " 代表空地, “ M ” 代表人, “ H ” 代表家。 现在,要你安排每个人从他所在的地方移动到家里,每移动一格的消耗是1,求最小的消耗。 人可以移动到家的那一格但是不进去。 解析: 先用bfs搞出每个M与每个H的距离。 然后就是网络流的建图过程了,先抽象出源点s和汇点t。 令源点与每个人相连,容量为1,费用为

poj 3068 有流量限制的最小费用网络流

题意: m条有向边连接了n个仓库,每条边都有一定费用。 将两种危险品从0运到n-1,除了起点和终点外,危险品不能放在一起,也不能走相同的路径。 求最小的费用是多少。 解析: 抽象出一个源点s一个汇点t,源点与0相连,费用为0,容量为2。 汇点与n - 1相连,费用为0,容量为2。 每条边之间也相连,费用为每条边的费用,容量为1。 建图完毕之后,求一条流量为2的最小费用流就行了

JavaSE(十三)——函数式编程(Lambda表达式、方法引用、Stream流)

函数式编程 函数式编程 是 Java 8 引入的一个重要特性,它允许开发者以函数作为一等公民(first-class citizens)的方式编程,即函数可以作为参数传递给其他函数,也可以作为返回值。 这极大地提高了代码的可读性、可维护性和复用性。函数式编程的核心概念包括高阶函数、Lambda 表达式、函数式接口、流(Streams)和 Optional 类等。 函数式编程的核心是Lambda

17 通过ref代替DOM用来获取元素和组件的引用

重点 ref :官网给出的解释是: ref: 用于注册对元素或子组件的引用。引用将在父组件的$refs 对象下注册。如果在普通DOM元素上使用,则引用将是该元素;如果在子组件上使用,则引用将是组件实例: <!-- vm.$refs.p will be the DOM node --><p ref="p">hello</p><!-- vm.$refs.child will be the c

一些数学经验总结——关于将原一元二次函数增加一些限制条件后最优结果的对比(主要针对公平关切相关的建模)

1.没有分段的情况 原函数为一元二次凹函数(开口向下),如下: 因为要使得其存在正解,必须满足,那么。 上述函数的最优结果为:,。 对应的mathematica代码如下: Clear["Global`*"]f0[x_, a_, b_, c_, d_] := (a*x - b)*(d - c*x);(*(b c+a d)/(2 a c)*)Maximize[{f0[x, a, b,

【JAVA入门】Day35 - 方法引用

【JAVA入门】Day35 - 方法引用 文章目录 【JAVA入门】Day35 - 方法引用一、方法引用的分类1.引用静态方法2.引用成员方法2.1 引用其他类的成员方法2.2 引用本类和父类的成员方法2.3 引用构造方法2.4 使用类名引用成员方法2.5 引用数组的构造方法 二、方法引用的例题         方法引用就是“把已经有的方法当作函数式接口中抽象方法的方法

gcc 编译器对 sqrt 未定义的引用

man sqrt  Link with -lm. gcc -o test test.c -lm 原因:缺少某个库,用 -l 参数将库加入。Linux的库命名是一致的, 一般为 libxxx.so, 或 libxxx.a, libxxx.la, 要链接某个库就用   -lxxx,去掉头 lib 及 "." 后面的 so, la, a 等即可。 常见的库链接方法为

Java应用对接pinpoint监控工具的时候,应用名称长度超出限制而导致接入失败

一、背景 java应用需要接入pinpoint,同一个虚拟机上的其他应用接入成功,唯独本应用不行。 首先排除是pinpoint agent的问题,因为其他应用都正常。 然后,我就对比二者的启动脚本。 -javaagent:/opt/pinpoint/pinpoint-bootstrap.jar -Dpinpoint.agentId=DA301004_17 -Dpinpoint.applic