C++复习 移动语义和右值引用

移动语义:意味着把某对象持有的资源或内容转移给另一个对象。
右值引用:只能在赋值运算符的右侧使用,没有名字的变量,不能取地址,不能解引用

纯右值(prvalue),将亡值(xvalue),和左值(lvalue)

C++语法中把表达式分为三种值类型,分别是:纯右值(prvalue),将亡值(xvalue),和左值(lvalue)。

左值有:有名字的变量; 可以取地址的变量; 指针解引用的值(使用语言内置的*或->运算符);
右值有:字面值(literal),比如1, true, nullptr——但字符串常量不是右值而是左值,它们被编译器放在数据段(data segment),可以取地址;非引用类型的函数返回值;各种内置的运算符组合表达式的计算结果。

右值引用:表达式等号右边的值需要是右值,可以使⽤std::move函数强制把左值转换为右值。
左值引用:并不拥有所绑定对象的堆存,所以必须⽴即初始化。 对于左值引⽤,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使⽤const引⽤形式

template <typename T>
struct my_collection
{
    std::vector<T> collection_;
    
    void add(T thing)       // 按值传递
    {
        collection_.push_back(thing);
    }
    void add(const T& thing) // 按常量左值引用传递
    {
        collection_.push_back(thing);
    }
    void add(T&& thing)     // 按右值传递
    {
        collection_.push_back( std::move(thing) );
    }
};

1、T&& thing不一定是右值引用:为什么参数为右值时,还要再std::move()一回?如上文所言,区分左右值的标准之一就是变量是否有名字。thing虽然是个右值引用(type),但它有名字,因此仍然是左值(value category);如果要移动它,就得调用std::move()把它转换为右值引用。
2、const T&&会失去移动语义,因为移动需会修改对象 :可以写const T&&吗?这个引用类型确实是合乎语法的,但是语义上存在矛盾:移动会修改对象,const修饰符又禁止了修改。因此,如果真的传入一个const T&&给add(T&& thing),那么将调用push_back(const T&)而不是push_back(T&&),从而失去移动语义。这里可以看到,右值引用是可以绑定到左值引用的。对于已有复制构造(赋值)函数,但没有重载移动构造(赋值)函数的类,传递右值引用作为参数会回退为调用复制构造(赋值)函数。
3、std::move()不移动任何东西,只是明确地表达移动的意图放弃该对象所持有资源的所有权,转移给其他对象。

移动语义

移动语义的引入:C++11引入了移动语义,主要目的是提高程序的效率,尤其是对于涉及资源管理(如内存分配)的类。当一个对象以右值的形式出现时(通常是临时对象或被std::move处理过的对象),理想情况下应该使用移动构造函数或移动赋值操作符来**“移动”资源**,而非进行耗时的深拷贝

移动语义的“移动”,意味着把某对象持有的资源或内容转移给另一个对象。以转移一个vector对象为例,移动构造red_vector时,并没有把原来的数组内容复制一遍,而是直接把指针指向orange_vector的数组,然后把orange_vector的指针置空,再设置red_vector的大小为该数组的大小

移动语义实现:

// 移动构造函数
    // std::exchange(obj, new_val)的作用是把返回obj的旧值,并把new_val赋值给obj
    my_vector(my_vector&& oth) :
        data_(std::exchange(oth.data_, nullptr)),
        size_(std::exchange(oth.size_, 0)), 
        capacity_(std::exchange(oth.capacity_, 0))
    {}  

C++提供了表达运行时错误的异常机制,可以通过throw关键字来抛出异常对象;相应地,在使用异常机制的语境下,发展出了“异常安全”的概念。比如,往一个std::vector对象中添加一个新元素时,如果其预先分配的数组已满,其会申请一块更大的内存;但如果内存耗尽,内存分配器抛出异常std::bad_alloc。根据标准,std::vector申请新内存失败后,原有数组应保持不变,这被称为strong exception guarantee。

void reallocate() {
    size_t new_capacity = capacity_ * 2;

    // 申请内存:内存不足时会抛异常!
    T* new_data = allocate(sizeof(T) * new_capacity);

    // 移动或复制已有元素到新数组
    T* first = data_;
    T* last = data_ + size_;
    if ( can_nothrow_move_construct<T> ) {     // 如果T的移动构造函数不抛出异常,则移动构造    
        move_construct(first, last, new_data); // 在new_data所指向的内存上移动构造[first, last)范围的元素
    }
    else { // 否则复制构造
        try {
            T* constructed_first = new_data;
            T* constructed_last = new_data;
            while (first != last) {
                copy(first++, constructed_last++); // 复制构造
            }
        }
        catch (...) {           // 如果复制构造抛出异常:
            destroy(constructed_first, constructed_last);  // 析构新数组中已经构造的元素
            free(new_data);     // 释放内存
            throw;              // 重新抛出异常给vector调用者
        }
    }
}

假设:移动第n个元素到新数组时,T的移动构造抛出异常,就会导致新数组中有n - 1个完好的元素,第n个元素状态未定义;而旧数组中则有n - 1个被移动过的元素,1个未定义的元素,还可能一些剩下未移动的元素。这时,vector的状态就难以恢复了。因此,为了保证异常安全,如果类型T的移动构造函数不应该抛出异常,否则,vector在扩容时仍然会复制,而不是移动元素!

在C++中,可以通过noexcept符号声明一个函数不会抛出异常。编译器不会给一个noexcept函数生成异常处理代码,并会检查该函数中调用的其他函数是否也是noexcept的。
unique_ptr的“复制”(unique_ptr禁止拷贝构造函数、拷贝赋值运算符,但实现了移动构造函数、移动赋值运算符用来高效转移std::unique_ptr 的所有权。)

template <typename T>
class my_unique_ptr
{
    T* ptr_;
public:
    // 移动构造函数
    my_unique_ptr(my_unique_ptr&& oth) noexcept;

    // 移动赋值函数
    my_unique_ptr& operator=(my_unique_ptr&& rhs) noexcept;
};

小结一下

移动语义让程序员在复制对象时有所选择,不一定需要发生深拷贝
右值引用是C++语法层面表示移动语义的机制
std::move()没有移动任何东西,只是把一个表达式转型(cast)为右值。
尽量让具有移动语义的函数noexcept

  • 18
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值