移动语义的一切
Posted zero_waring
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动语义的一切相关的知识,希望对你有一定的参考价值。
移动语义的一切
参考
c++ primer
modern-cpp-tutorial-zh-cn
必修课 : 左值和右值
本质:左值指的是对象在内存的位置;右值指的是对象的内容
一般来说在赋值运算符左边的就是左值:
- 赋值运算符需要非常量左值作为左侧运算对象,得到的结果也仍然是左值
- 取地址符号
- 内置解引用、下标运算符、自增
- 内置迭代器的递减递增操作符
那么右值呢?
c++11分为纯右值和将亡值
纯右值:
- 纯粹的字面量; 10, true
- 求值结果相当于字面量或者匿名临时对象。 1 + 2
- 运算表达式产生的临时变量、Lamba表达式
将亡值:
-
变量在当前作用域结束后被销毁
std::vector<int> foo() { std::vector<int> temp = {1, 2, 3, 4}; return temp; } std::vector<int> v = foo();
这其中经历了 temp的销毁以及v的拷贝构造的调用,但是其实 temp的资源我们可以“移动“出来
c++11提出的移动就是针对上面场景的
并且在c++11中 上面的使用会进行优化,会把temp直接移动构造给 std::vector
v,类似static_cast<std::vector &&>(temp)
左值引用和右值引用
左值引用就是绑定到左值
但是右值引用我们用它绑定要将亡值上,延长将要销毁的对象的生命周期,表达式返回值、临时对象
看几个例子
int i = 42;
int& r = i;
int&& rr = i; //错误,不能把右值引用绑定到左值上
int& r2 = i * 42; //错误 i*42是一个右值
const int& r3 = i * 42;//正确,可以将一个const引用绑定到右值上
int&& rr2 = i * 42//正确
返回左值的场景:
赋值、下标、解引用、前置递增/递减运算符,都返回左值
返回右值的场景:
算数、关系、位、后置递增/递减运算符,可以使用const 左值或者右值绑定到这类上
左值持久;右值短暂
左值有持久的状态,右值要么是纯右值要么是临时对象
我们要解决的就是:用右值接收那些即将销毁的对象并且这些对象没有其他用户
变量是左值
变量可以看作只有运算对象但是没有运算符表达式。类似其他表达式,变量本身也有左值和右值的属性。
变量表达式都是左值,结果就是不能将右值引用绑定在右值引用类型的变量上
int&& r = 1;
int&& r1 = r; //错误r是左值
因为左值是持久的,右值是临时的
标准库move函数
作用:显示的把左值转换给对应的右值类型
int i = 42;
int&& rr = std::move(i); //左值转换为右值引用
调用move意味着,除了对i进行赋值或者销毁他,我们将不再使用它。
移动构造和移动赋值运算符
c++11的伟大在于这点,我们在类中自己构建 类似拷贝构造函数,但是参数为右值引用的参数,对于移动赋值运算符也是一样的
例子1:
class Object{
public:
mutable int* pointer;
Object():pointer(new int(1)) {
std::cout << "Object()" << std::endl;
}
Object(const Object& other):pointer(new int(*other.pointer)) {
std::cout << "Object(const Object& other)" << std::endl;
}
Object(const Object&& other):pointer(other.pointer) {
other.pointer = nullptr;
std::cout << "Object(const Object&& other)" << std::endl;
}
~Object() {
std::cout << "~Object()" << std::endl;
delete pointer;
}
};
Object return_lvalue(bool test) {
Object a, b;
if( test ) return a;
else {
return b;
}
}
int main()
{
Object obj = return_lvalue(true);
std::cout << obj.pointer << std::endl;
std::cout << *obj.pointer << std::endl;
return 0;
}
-
关于移动操作、标准库容器和异常
由于移动操作仅仅对资源进行转移,它通常不分配资源。那么我们应该告诉标准库我们这个函数不会抛出异常,
在构造函数中指明 noexcept,出现在参数列表和初始化列表开始的冒号之间
class Str{ public: Str(Str&&) noexcept; }; Str::Str(Str&&) noexcept { }
必须在声明和定义都加上noexcept
为了避免移动构造函数中发生异常带来的内存错误,我们应该显示noexcept告诉标准库移动构造函数可以安全使用
-
移动赋值操作符
str& operator=(str&& other) noexcept { if( this != &other ) { free(); //接管资源 //将other资源置为空 } }
判断自我赋值的原因是,参数可能是std::move来的
移动的一些注意
-
移动后原对象必须可析构在移动后,源对象内部可能已经改变,但是这不影响它被重新赋值,只不过我们不应该对它内部函数做一些正确的假设
-
编译器会默认给我们生成移动拷贝和移动赋值操作符吗
- 当一个类定义自己的拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为他合成移动构造和移动赋值操作符。我们调用的时候使用的就是拷贝操作
- 当一个类没有定义任何自己版本的拷贝控制成员,且每个非static数据成员都可以移动,编译器才会为他合成移动构造或移动赋值操作**
- 如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个类成员
- 如果类成员是const或引用,则类的移动赋值运算符被定义为删除的
- 定义了移动赋值和移动拷贝的类,也要定义自己的拷贝构造和赋值操作符,否则那将是被删除的
比如
struct X{ int i; std::string s; }; struct hasX{ X mem; }; X x, x2 = std::move(x); hasX hx,hx2 = std::move(hx);
-
移动右值,拷贝左值
我们常常一个类既有拷贝构造又有移动构造
匹配的规则:左值只能匹配拷贝构造
右值 const拷贝和移动构造都能匹配,但是移动构造更精确,使用移动构造
一定记住,const 左值可以绑定到右值上
-
拷贝并交换赋值运算符和移动操作
#include <iostream>
class Object{
public:
mutable int* pointer;
Object():pointer(new int(1)) {
std::cout << "Object()" << std::endl;
}
Object(const Object& other):pointer(new int(*other.pointer)) {
std::cout << "Object(const Object& other)" << std::endl;
}
Object(Object&& other)noexcept :pointer(other.pointer) {
other.pointer = nullptr;
std::cout << "Object(const Object&& other)" << std::endl;
}
Object& operator=(Object other) {
if( this != &other ) {
using namespace std;
swap(*this, other);
std::cout << "Object& operator=(Object other)" << std::endl;
}
return *this;
}
~Object() {
std::cout << "~Object()" << std::endl;
delete pointer;
}
friend void swap(Object&, Object&);
};
inline void swap(Object& left, Object& right) {
using std::swap;
swap(left.pointer, right.pointer);
}
int main()
{
Object a;
Object b;
Object c = std::move(b);
a = c;
return 0;
}
看上面的例子 Object& operator=(Object other),移动赋值和拷贝赋值共用这一个函数,在 Object other的情况下,涉及到拷贝构造还是移动构造
然后使用内置的swap进行交换
5. 右值引用和成员函数
成员函数中一般是 const T& 和 T&&,前者是接收左值或右值,后者只接收右值并移动
我们调用容器的时候就自己选择是要拷贝还是移动
1. vector性能例子
int main()
{
std::string str = "Hello World";
std::vector<string> ret;
ret.push_back(str);
std::cout << "str = " << str << std::endl;
ret.push_back(std::move(str));
std::cout << "str = " << str << std::endl;
return 0;
}
2. std::unique_ptr
unique_ptr<int> create_obj() {
unique_ptr<int> ptr(new int(1));
return ptr;
}
//编译器在c++ 11把ptr 移动构造了 unique_ptr<int> ,因为unique_ptr默认是不允许 拷贝和赋值的
int main()
{
unique_ptr<int> a = create_obj();
return 0;
}
3. std::vector的增长
当vector的存储容量需要增长时,通常会重新申请一块内存,并把原来的内容一个个复制过去并删除。对,复制并删除,改用移动就够了。
对于像vector
4. std::unique_ptr放入容器
由于vector增长时会复制对象,像std::unique_ptr这样不可复制的对象是无法放入容器的只是“移动”对象。所以随着移动语义的引入,std::unique_ptr放入std::vector成为理所当然的事情
没有智能指针的容器
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
存储空间等价于裸指针,但安全性强了一个世纪。实际中需要共享所有权的对象(指针)是比较少的,但需要转移所有权是非常常见的情况。auto_ptr的失败就在于其转移所有权的繁琐操作。unique_ptr配合移动语义即可轻松解决所有权传递的问题
unique_ptr<int> create_obj() {
unique_ptr<int> ptr(new int(1));
return ptr;
}
int main()
{
vector<unique_ptr<int>> ret;
ret.push_back(create_obj());
return 0;
}
完美把create_obj值给了 vector,使用移动语义
以上是关于移动语义的一切的主要内容,如果未能解决你的问题,请参考以下文章
SAPUI5 如何在语义详细视图中插入片段或 xmlview