移动语义的一切

Posted zero_waring

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动语义的一切相关的知识,希望对你有一定的参考价值。

移动语义的一切

参考

​ c++ primer

​ modern-cpp-tutorial-zh-cn

必修课 : 左值和右值

本质:左值指的是对象在内存的位置;右值指的是对象的内容

一般来说在赋值运算符左边的就是左值:

  1. 赋值运算符需要非常量左值作为左侧运算对象,得到的结果也仍然是左值
  2. 取地址符号
  3. 内置解引用、下标运算符、自增
  4. 内置迭代器的递减递增操作符

那么右值呢?

​ c++11分为纯右值和将亡值

纯右值:

  1. 纯粹的字面量; 10, true
  2. 求值结果相当于字面量或者匿名临时对象。 1 + 2
  3. 运算表达式产生的临时变量、Lamba表达式

将亡值:

  1. 变量在当前作用域结束后被销毁

    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;
}
  1. 关于移动操作、标准库容器和异常

    由于移动操作仅仅对资源进行转移,它通常不分配资源。那么我们应该告诉标准库我们这个函数不会抛出异常,

    在构造函数中指明 noexcept,出现在参数列表和初始化列表开始的冒号之间

    class Str{
    public:
    	Str(Str&&) noexcept;
    };
    Str::Str(Str&&) noexcept {
    }
    
    

    必须在声明和定义都加上noexcept

    为了避免移动构造函数中发生异常带来的内存错误,我们应该显示noexcept告诉标准库移动构造函数可以安全使用

  2. 移动赋值操作符

    str& operator=(str&& other) noexcept {
    	if( this != &other ) {
    		free();
    		//接管资源
    		//将other资源置为空
    	} 
    }
    

    判断自我赋值的原因是,参数可能是std::move来的

移动的一些注意

  1. 移动后原对象必须可析构在移动后,源对象内部可能已经改变,但是这不影响它被重新赋值,只不过我们不应该对它内部函数做一些正确的假设

  2. 编译器会默认给我们生成移动拷贝和移动赋值操作符吗

    1. 当一个类定义自己的拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为他合成移动构造和移动赋值操作符。我们调用的时候使用的就是拷贝操作
    2. 当一个类没有定义任何自己版本的拷贝控制成员,且每个非static数据成员都可以移动,编译器才会为他合成移动构造或移动赋值操作**
    3. 如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个类成员
    4. 如果类成员是const或引用,则类的移动赋值运算符被定义为删除的
    5. 定义了移动赋值和移动拷贝的类,也要定义自己的拷贝构造和赋值操作符,否则那将是被删除的

    比如

    struct X{
    int i;
    std::string s;
    };
    struct hasX{
    X mem;
    };
    X x, x2 = std::move(x);
    hasX hx,hx2 = std::move(hx);
    
  3. 移动右值,拷贝左值

    ​ 我们常常一个类既有拷贝构造又有移动构造

    ​ 匹配的规则:左值只能匹配拷贝构造

    ​ 右值 const拷贝和移动构造都能匹配,但是移动构造更精确,使用移动构造

    ​ 一定记住,const 左值可以绑定到右值上

  4. 拷贝并交换赋值运算符和移动操作

#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>,完全无需显式析构,unqiue_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

有没有办法禁止键盘移动LinearLayout?

详解 移动端语义化标签

为啥 HLSL 有语义?

Cg入门19:Fragment shader - 片段级模型动态变色

软输入键盘隐藏编辑文本