求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
要资料
 
追随技术信仰

随时听讲座
每天看新闻
 
 
C++并发编程(中文版)
前言
第1章 你好,C++的并发世界!
1.1 何谓并发
1.2 为什么使用并发?
1.3 C++中的并发和多线程
1.4 开始入门
1.5 本章总结
第2章 线程管理
2.1 线程管理的基础
2.2 向线程函数传递参数
2.3 转移线程所有权
2.4 运行时决定线程数量
2.5 识别线程
2.6 本章总结
第3章 线程间共享数据
3.1 共享数据带来的问题
3.2 使用互斥量保护共享数据
3.3 保护共享数据的替代设施
3.4 本章总结
第4章 同步并发操作
4.1 等待一个事件或其他条件
4.2 使用期望等待一次性事件
4.3 限定等待时间
4.4 使用同步操作简化代码
4.5 本章总结
第5章 C++内存模型和原子类型操作
5.1 内存模型基础
5.2 C++中的原子操作和原子类型
5.3 同步操作和强制排序
5.4 本章总结
第6章 基于锁的并发数据结构设计
6.1 为并发设计的意义何在?
6.2 基于锁的并发数据结构
6.3 基于锁设计更加复杂的数据结构
6.4 本章总结
第7章 无锁并发数据结构设计
7.1 定义和意义
7.2 无锁数据结构的例子
7.3 对于设计无锁数据结构的指导建议
7.4 本章总结
第8章 并发代码设计
8.1 线程间划分工作的技术
8.2 影响并发代码性能的因素
8.3 为多线程性能设计数据结构
8.4 设计并发代码的注意事项
8.5 在实践中设计并发代码
8.6 本章总结
第9章 高级线程管理
9.1 线程池
9.2 中断线程
9.3 本章总结
第10章 多线程程序的测试和调试
10.1 与并发相关的错误类型
10.2 定位并发错误的技术
10.3 本章总结
第11章 C++11语言特性简明参考(部分)
11.1 右值引用
11.2 删除函数
11.3 默认函数
11.4 常量表达式函数
11.5 Lambda函数
11.6 变参模板
11.7 自动推导变量类型
11.8 线程本地变量
 

 
目录
右值引用
作者:Anthony Williams  译者:陈晓伟
43 次浏览
4次  

A.1 右值引用

如果你从事过C++编程,你会对引用比较熟悉,C++的引用允许你为已经存在的对象创建一个新的名字。对新引用所做的访问和修改操作,都会影响它的原型。

例如:

  1. int var=42;
  2. int& ref=var; // 创建一个var的引用
  3. ref=99;
  4. assert(var==99); // 原型的值被改变了,因为引用被赋值了

 

目前为止,我们用过的所有引用都是左值引用——对左值的引用。lvalue这个词来自于C语言,指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。rvalue这个词也来源于C语言,指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。因此,左值引用只能被绑定在左值上,而不是右值。

不能这样写:

  1. int& i=42; // 编译失败

例如,因为42是一个右值。好吧,这有些假;你可能通常使用下面的方式讲一个右值绑定到一个const左值引用上:

  1. int const& i = 42;

这算是钻了标准的一个空子吧。不过,这种情况我们之前也介绍过,我们通过对左值的const引用创建临时性对象,作为参数传递给函数。

其允许隐式转换,所以你可这样写:

  1. void print(std::string const& s);
  2. print("hello"); //创建了临时std::string对象

C++11标准介绍了右值引用(rvalue reference),这种方式只能绑定右值,不能绑定左值,其通过两个&&来进行声明:

  1. int&& i=42;
  2. int j=42;
  3. int&& k=j; // 编译失败

因此,可以使用函数重载的方式来确定:函数有左值或右值为参数的时候,看是否能被同名且对应参数为左值或有值引用的函数所重载。

其基础就是C++11新添语义——移动语义(move semantics)。

A.1.1 移动语义

右值通常都是临时的,所以可以随意修改;如果知道函数的某个参数是一个右值,就可以将其看作为一个临时存储或“窃取”内容,也不影响程序的正确性。这就意味着,比起拷贝右值参数的内容,不如移动其内容。动态数组比较大的时候,这样能节省很多内存分配,提供更多的优化空间。试想,一个函数以std::vector<int>作为一个参数,就需要将其拷贝进来,而不对原始的数据做任何操作。C++03/98的办法是,将这个参数作为一个左值的const引用传入,然后做内部拷贝:

  1. void process_copy(std::vector<int> const& vec_)
  2. {
  3. std::vector<int> vec(vec_);
  4. vec.push_back(42);
  5. }

这就允许函数能以左值或右值的形式进行传递,不过任何情况下都是通过拷贝来完成的。如果使用右值引用版本的函数来重载这个函数,就能避免在传入右值的时候,函数会进行内部拷贝的过程,因为可以任意的对原始值进行修改:

  1. void process_copy(std::vector<int> && vec)
  2. {
  3. vec.push_back(42);
  4. }

 

如果这个问题存在于类的构造函数中,窃取内部右值在新的实例中使用。可以参考一下清单中的例子(默认构造函数会分配很大一块内存,在析构函数中释放)。

清单A.1 使用移动构造函数的类

  1. class X
  2. {
  3. private:
  4. int* data;
  5. public:
  6. X():
  7. data(new int[1000000])
  8. {}
  9. ~X()
  10. {
  11. delete [] data;
  12. }
  13. X(const X& other): // 1
  14. data(new int[1000000])
  15. {
  16. std::copy(other.data,other.data+1000000,data);
  17. }
  18. X(X&& other): // 2
  19. data(other.data)
  20. {
  21. other.data=nullptr;
  22. }
  23. };

一般情况下,拷贝构造函数①都是这么定义:分配一块新内存,然后将数据拷贝进去。不过,现在有了一个新的构造函数,可以接受右值引用来获取老数据②,就是移动构造函数。在这个例子中,只是将指针拷贝到数据中,将other以空指针的形式留在了新实例中;使用右值里创建变量,就能避免了空间和时间上的多余消耗。

X类(清单A.1)中的移动构造函数,仅作为一次优化;在其他例子中,有些类型的构造函数只支持移动构造函数,而不支持拷贝构造函数。例如,智能指针std::unique_ptr<>的非空实例中,只允许这个指针指向其对象,所以拷贝函数在这里就不能用了(如果使用拷贝函数,就会有两个std::unique_ptr<>指向该对象,不满足std::unique_ptr<>定义)。不过,移动构造函数允许对指针的所有权,在实例之间进行传递,并且允许std::unique_ptr<>像一个带有返回值的函数一样使用——指针的转移是通过移动,而非拷贝。

如果你已经知道,某个变量在之后就不会在用到了,这时候可以选择显式的移动,你可以使用static_cast<X&&>将对应变量转换为右值,或者通过调用std::move()函数来做这件事:

  1. X x1;
  2. X x2=std::move(x1);
  3. X x3=static_cast<X&&>(x2);

 

想要将参数值不通过拷贝,转化为本地变量或成员变量时,就可以使用这个办法;虽然右值引用参数绑定了右值,不过在函数内部,会当做左值来进行处理:

  1. void do_stuff(X&& x_)
  2. {
  3. X a(x_); // 拷贝
  4. X b(std::move(x_)); // 移动
  5. }
  6. do_stuff(X()); // ok,右值绑定到右值引用上
  7. X x;
  8. do_stuff(x); // 错误,左值不能绑定到右值引用上

 

移动语义在线程库中用的比较广泛,无拷贝操作对数据进行转移可以作为一种优化方式,避免对将要被销毁的变量进行额外的拷贝。在2.2节中看到,在线程中使用std::move()转移std::unique_ptr<>得到一个新实例;在2.3节中,了解了在std:thread的实例间使用移动语义,用来转移线程的所有权。

std::thread、std::unique_lock<>、std::future<>、 std::promise<>和std::packaged_task<>都不能拷贝,不过这些类都有移动构造函数,能让相关资源在实例中进行传递,并且支持用一个函数将值进行返回。std::string和std::vector<>也可以拷贝,不过它们也有移动构造函数和移动赋值操作符,就是为了避免拷贝拷贝大量数据。

C++标准库不会将一个对象显式的转移到另一个对象中,除非将其销毁的时候或对其赋值的时候(拷贝和移动的操作很相似)。不过,实践中移动能保证类中的所有状态保持不变,表现良好。一个std::thread实例可以作为移动源,转移到新(以默认构造方式)的std::thread实例中。还有,std::string可以通过移动原始数据进行构造,并且保留原始数据的状态,不过不能保证的是原始数据中该状态是否正确(根据字符串长度或字符数量决定)。

A.1.2 右值引用和函数模板

在使用右值引用作为函数模板的参数时,与之前的用法有些不同:如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用;当提供右值的时候,会当做普通数据使用。可能有些口语化,来看几个例子吧。

考虑一下下面的函数模板:

  1. template<typename T>
  2. void foo(T&& t)
  3. {}

 

随后传入一个右值,T的类型将被推导为:

  1. foo(42); // foo<int>(42)
  2. foo(3.14159); // foo<double><3.14159>
  3. foo(std::string()); // foo<std::string>(std::string())

 

不过,向foo传入左值的时候,T会被推导为一个左值引用:

  1. int i = 42;
  2. foo(i); // foo<int&>(i)

 

因为函数参数声明为T&&,所以就是引用的引用,可以视为是原始的引用类型。那么foo()就相当于:

  1. foo<int&>(); // void foo<int&>(int& t);

这就允许一个函数模板可以即接受左值,又可以接受右值参数;这种方式已经被std::thread的构造函数所使用(2.1节和2.2节),所以能够将可调用对象移动到内部存储,而非当参数是右值的时候进行拷贝。


您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码: 验证码,看不清楚?请点击刷新验证码 必填



43 次浏览
4次