c++11:对象移动 & 右值引用 & 移动构造函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_19923217/article/details/82591245

一、概述

c++ 11 新标准中最主要的特征是可以移动而非拷贝对象的能力。很多情况下,对象拷贝后就会立即被销毁。
在这些情况下,移动而非拷贝对象会大幅度提升性能。

在旧 C++ 标准中,没有直接的方法移动对象。因此,即使不必要拷贝对象的情况下,我们也不得不拷贝。如果对象本身要求分配内存,进行不必要的拷贝代价非常高。类似的,在旧版本的标准库中,容器所保存的类型必须是可拷贝的,但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。

移动,简单来说是解决各种情形下对象的资源所有权转移问题。

二、左值、右值与右值引用

2.1 左值与右值

左值与右值是从 C 语言继承过来的概念,原本是为了帮助记忆:左值可以出现在赋值语句的左边与右边,而右值只能出现在赋值语句的右边,如下面的赋值表达式:

a = b + c;

a 就是一个左值,而 b + c 则是一个右值。这种识别左值右值的方法在 C++ 中依然有效。不过 C++ 中有更广泛认同的说法,那就是:

可以取地址的、有名字的就是左值,反之不能取地址的、没有名字的就是右值。

分析上面的加法表达式,&a 是允许的操作,但 &(b + c) 这样的操作则不会通过编译。因此 a 是一个左值,(b + c) 则是右值。

这种判别方式非常有效,但是很感性,下面介绍更为细致的 C++11 中的判定方式。

–> 区分左值、纯右值与将亡值
  1. 纯右值:C++ 98 标准中的右值的概念,用于辨识临时变量与一些不跟对象关联的值,几种比较编程比较典型的纯右值:
1. 非引用返回的函数返回的临时变量值
2. 运算表达式,如 (1+2) 产生的临时变量值
3. 不与对象相关联的字面量值,如:2、true
4. 类型转换函数的返回值
5. lambda 表达式
  1. 将亡值:C++11 新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象(资源所有权转移),通常是返回右值引用 T&& 的函数返回值、std::move 的返回值等。
  2. 左值:除纯右值与将亡值外可以标识函数、对象的值都属于左值。

在 C++11 程序中,所有的值必属于左值、将亡值、纯右值三者之一。

2.2 右值引用

为了支持移动语义,新标准中引入了一种必须绑定到右值得引用叫做右值引用,相比左值引用,右值引用通过 && 而不是 & 来获取,如我们将看到的,右值引用有一个重要的性质——只能绑定到一个将要销毁的对象上,因此我们可以很安全的将右值引用的资源”移动”到另一个对象中。

int i = 12;
int &r = i;         // 左值引用
int &&r1 = i * 12;  // 右值引用

const int &r3 = i * 10; // 我们可以将一个 conts 引用绑定到右值中
–> 常量左值引用

《const int &r3》被称为常量引用类型,它可以接受左值、右值对其进行初始化。而且在使用右值对其进行初始化时,常量左值引用也可以像右值引用一样将右值的生命期延长,不过相比于右值引用所引用的右值,常量左值引用所引用的右值在它接下来的声明周期只能是只读的。

T &e = ReturnRValue();          // 编译失败
const T &f = ReturnRValue();    // 编译通过
–> 左值持久;右值短暂

观察左值与右值的特征,两者的区别很明显:左值是持久的状态,而右边要么是字面常量,要么是临时变量,是将要被销毁的对象。

++这意味着:使用右值引用的代码可以自由安全的接管所引用对象的资源。++

2.3 std::move:强制转化为右值

在 C++11 中,标准库在 中提供了一个有用的函数 std::move,这个函数并不移动任何东西,它唯一的功能就是将一个左值强制转化为右值引用,继而可以通过右值继续使用该值,以用于移动语义。

T &&r = std::move(lvalue);

从实现上来讲,std::move 基本等价于一个类型转换:

static_cast<T&&>(lvalue);

三、移动语义

前面引申出了右值引用,而在 C++11 的设计中,右值引用的意义有两大作用:移动语义和完美转发。我们主要讲一下移动语义。

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。

举个栗子。

问题:如何将大象从冰箱A转移到另一台冰箱B

  • 回答一:打开冰箱A门,取出大象,关上冰箱A门,打开另一台冰箱B门,放进大象,关上冰箱B门。
  • 回答二:将冰箱B直接套到大象上,然后将冰箱A销毁。

回答二是一种典型的”移动”思想,右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义,以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。

对于左值,如果我们明确放弃对其资源的所有权,则可以通过 std::move() 来将其转为右值引用。std::move() 实际上是 static_cast

3.1 移动构造函数和移动赋值运算符

移动构造函数和移动赋值运算符是移动语义最典型的一个体现。

类似 string 类,如果我们自己的类也同时支持移动和拷贝,那么也能从中受益。为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。与拷贝构造函数不同的是,它们从给定对象窃取资源而不是拷贝资源。

移动构造函数的第一个参数是该类类型的一个右值引用。

class Foo {
public:
    Foo() : ptr(new int(0)) {
        std::cout << "<--- Construct --->" << std::endl;
    }

    // 移动构造函数 
    Foo(Foo &&foo) : ptr(foo.ptr) {
        // 令 ptr 进入这样的状态——对其进行析构函数时安全的
        foo.ptr = nullptr;
        std::cout << "<--- Move construct --->" << std::endl;
    }

    int *ptr;
}

与拷贝构造函数不同,移动构造函数不分配任何新内存,它从给定的 foo 对象中接管内存,并将给定 foo 对象指针都置为 nullptr,这使得对给定对象执行析构函数时安全的。

移后源对象必须可析构

值得注意的是,从一个给定对象移动数据并不会销毁此对象,但必须保证,经过”移动”操作后,源对象是可以被安全销毁的,也就是当编写一个移动操作后,必须保证移后源对象进入一个可析构的状态。

为了满足这一要求,我们通过将移后源对象的指针置为 nullptr 来实现。

默认移动构造函数

与拷贝构造函数不同的是,只有类的每个非 static 数据都可移动时,编译器才会为它合成移动构造函数或移动赋值表达式。编译器可以移动内置类型的成员,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。

class Foo {
    int i;          // 内置类型可以移动
    std::string s;  // string 类定义了自己的移动操作
}

Foo foo;
// 使用移动构造函数
foo1 = Foo(std::move(foo));

如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动。

移动右值,拷贝左值…

如果一个类既有移动构造函数,又有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。

有一个比较简单口诀是:移动右值,拷贝左值

Foo f1,f2;
Foo f2 = f1;    // f1 是左值,使用拷贝构造
Foo getFoo();   // getFoo 函数返回一个右值
f1 = getFoo();  // getFoo 返回右值,使用移动赋值

3.2 –

auto action = std::make_unique<Action>(false, filename, line);
if (!action->InitTriggers(triggers, err)) {
    return false;
}

action_ = std::move(action);

猜你喜欢

转载自blog.csdn.net/qq_19923217/article/details/82591245