【C++】C++11——右值引用及其相关功能

在这里插入图片描述

【C++】右值引用及其相关功能

1 左值、右值

1.1 左值及左值引用

先看以下代码,但凡使用过 C++ 的人应该都很清楚,第三行代码是无法通过编译的,原因是非常量引用只能绑定在左值上。这里的左值表示有具体物理内存地址的值,即变量,因此使用符号 “&” 表示只能绑定在具体变量上的引用被称作左值引用

int num_1 = 1int &num_2 = num_1;
int &num_3 = 1;

根据以下代码,在 C++11/C++0x 标准以前,如果需要将该左值引用绑定到一个常量上,通常的做法是使用常量引用以延长临时变量的生存期,这个操作在内存中同样也开辟了一块内存空间用于储存 1 和 get 函数的返回值。但遗憾的是,这些值都属于常量,无法被修改。

const int &num = 1;
const int &num = get(); // 设 get 函数的定义:int get() { return 1; }

1.2 右值及右值引用

C++11/C++0x 标准中提出了右值引用,与左值引用不同,右值引用绑定的对象是一个字面值、临时变量、将亡值(即右值)与常量引用类似,右值引用也能延长该临时值的生存期,但不同的是,所绑定的值是可以通过该右值引用修改的。可参考以下代码:

// 右值引用绑定在字面值 1 上,以延长该字面值 1 的生存期
int &&num = 1;
// 右值引用绑定在上临时值上
int &&num = get(); // 设 get 函数的定义:int get() { return 1; }
// 可随时对已被延长生存期的右值引用进行访问,右值引用变量在用于表达式时是左值
num = 2;

tips: 左值可以出现在赋值号 “=” 左边或者右边,而右值只可以出现在赋值号 “=” 右边。

拿一个不太恰当但容易理解的比喻:可以看做左值是个未爆炸的炸弹,右值是个即将爆炸的炸弹

2 拷贝、移动构造函数

我们首先定义一个类,这个类封装了一个数组,并且提供有参、复制两个构造函数

class MyArray
{
    int *_arr;
    int _size;
    
public:
    // 有参构造
    explicit MyArray(int size) : _size(size)
    {
        cout << "parameter constructor" << endl;
        _arr = new int[size];
    }
    // 复制构造
    MyArray(const MyArray& my_array) : _size(my_array._size)
    {
        cout << "copy constructor" << endl;
        _arr = new int[_size];
        // 复制每一个元素
        for (int  i = 0; i < _size; i++)
            _arr[i] = my_array._arr[i];
    }
    // 析构
    ~MyArray()
    {
        cout << "destructor" << endl;
        if (_arr != nullptr)
        {
            delete _arr;
            _arr = nullptr;
        }
    }
    inline int &operator[](size_t __n) { return _arr[__n]; }
};

在 main 函数中写入

int main(int argc, char *argv[]) // line 1
{                                // line 2
    vector<MyArray> arrs;        // line 3
    arrs.reserve(5);             // line 4
    MyArray arr(1);              // line 5
    arrs.push_back(arr);         // line 6
    arrs.push_back(MyArray(1));  // line 7
    return 0;                    // line 8
}                                // line 9

运行结果如下:

  • 第一行是 line 5 运行的结果,调用有参构造
  • 第二行是 line 6 运行的结果,将arr插入vector中,vector需要开辟一块新的空间来存储arr的内容,为复制构造
  • 第三行是 line 7MyArray(1)运行的结果,其创建了一个临时变量,调用有参构造
  • 第四行是 line 7 中 push_back 运行的结果,将该临时变量复制到 vector 中,同样调用复制构造
  • 第五行是临时变量在 line 7 运行结束之后销毁,调用析构函数所运行的结果
  • 第六行、第七行是 vector 中的元素在return 0之后释放内存,调用析构函数
  • 第八行是 line 5 的局部变量销毁
parameter constructor
copy constructor
parameter constructor
copy constructor
destructor
destructor
destructor
destructor

可以看到,对于临时变量的处理,传统的 C++ 显得十分笨拙,即创建一个新的空间,将该临时变量逐一复制到新空间,再销毁该临时变量,对于临时变量而言,如果能够将其数据所有权直接交给新空间,其物理内存地址不发生任何变动,那么将节省大量拷贝所浪费的时间。

其中,这个移交所有权的过程,在 C++ 中可以表示成将指针直接指向这个临时变量所在的地址,再让该临时变量“指向”空。这里的“指向”并不是C语言中指针指向某个地址的意思,而是该临时变量的变量名直接取址所指向的就是这块地址。这个操作可以看成该临时变量的内容直接移动到新空间中。

利用右值和右值引用的特点,可以加入一个新的构造函数,被称作移动构造函数。在上文 MyArray 类的代码中加入以下内容:

// move constructor
MyArray(MyArray &&my_array) : _size(my_array._size)
{
    cout << "move constructor" << endl;
    _arr = my_array._arr;
    my_array._arr = nullptr;
}

运行结果如下,在运行中传入push_back的参数为一个临时变量,因此编译器优先匹配移动构造函数,因此第四行为移动构造函数的运行结果

parameter constructor
copy constructor
parameter constructor
move constructor
destructor
destructor
destructor
destructor

3 移动语义

C++11 中提供了一个函数:std::move() ,其作用就是将为了移交变量对于地址访问的所有权,也就是将左值变成右值,其做的转换等价于static_cast<T &&>,因此不会修改变量的值。现给出以下代码,同样以上文的 MyArray 类为例

int main(int argc, char *argv[])
{
    MyArray arr1(1);
    MyArray arr2(arr1);
    MyArray arr3(move(arr1));
    
    MyArray &&tmp = move(arr2);
    MyArray arr4(tmp);

    return 0;
}

运行结果如下:

  • 首先,程序第一行调用有参构造函数。(结果第1行)

  • 第二行利用现有的 arr1 构造 arr2,同时 arr1 不受影响,调用复制构造函数。(结果第2行)

  • 然后程序调用了std::move(),因此其得到了一个右值(临时值)尝试点炸弹

  • 并且将其传给 arr3 调用移动构造,构造完成后 arr1 的数组内容将被释放。(结果第3行) 炸弹点着

  • 接着调用std::move(),得到了 arr2 的临时值,该临时值的生存期得以延长。尝试点炸弹

  • 最后,访问 tmp 时,此处 tmp 已不再是右值,而是 arr2 的左值引用,因此调用复制构造函数。(结果第4行) 炸弹没点着

parameter constructor
copy constructor
move constructor
copy constructor
destructor
destructor
destructor
destructor

4 万能引用、完美转发

4.1 引用折叠机制

C++11开始便提供了引用折叠机制,具体示例代码如下

using lref = int &;
using rref = int &&;

int n = 10;
lref &r1 = n;  // r1 的类型是 int&
lref &&r2 = n; // r2 的类型是 int&
rref &r3 = n;  // r3 的类型是 int&
rref &&r4 = 1; // r4 的类型是 int&&

4.2 转发引用

转发引用(也称作万能引用)是一种特殊的引用,具有下列两种情形:

  1. 函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 cv 限定( const 以及 volatile )的右值引用(T &&

    template <typename T>
    int foo(T &&x) { return x; }
    
    template <typename T>
    int f(T &&x) { return foo(forward<T>(x)); } // x 是转发引用,从而能被转发
    
    int main()
    {
        int i = 10;
        f(i); // 实参是左值,调用 f<int &>(int &), std::forward<int &>(x) 是左值
        f(5); // 实参是右值,调用 f<int>(int &&), std::forward<int>(x) 是右值
    }
    
    template <typename T>
    int g(const T &&x) { return x; } // x 不是转发引用:T 有 cv 限定
    
    template <typename T>
    struct A
    {
        template <typename U>
        A(T &&x, U &&y, int *p); // x 不是转发引用:T 不是该构造函数的类型模板形参,但 y 是转发引用
    };
    
  2. auto &&,但当其从花括号包围的初始化器列表推导时除外

    auto &&vec = x_f();                    // x_f() 可以是左值或右值,vec 是转发引用
    auto i = std::begin(vec);              // 也可以
    (*i)++;                                // 也可以
    x_g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别
    
    for (auto &&x : x_h())
    {
        // x 是转发引用;这是使用范围 for 循环最安全的方式
    }
    
    auto &&z = {1, 2, 3}; // 不是转发引用(初始化器列表的特殊情形)
    

总结:万能引用能保持函数实参的值类别,使得 std::forward 能用来完美转发实参。

参考文档

  1. 引用折叠、转发引用
  2. cv限定符
  3. forward转发函数

由于笔者水平有限,文中难免会有错误和疏漏之处,烦请各位读者评论指出!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Cccolt_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值