右值引用以及move移动语义和forward 完美转发

简介: 右值引用以及move移动语义和forward 完美转发


右值引用

右值引用最简单的作用:可以避免无谓的复制,提高了程序性能(在移动构造函数中有体现)。

什么是右值

最基本的解释:

左值可以取地址、位于等号左边;

右值没法取地址,位于等号右边。(或者函数的返回值等)

例如:

struct A {
    A(int a = 0) {
        a_ = a;
    }
    int a_;
};
A a = A();
  • 其中a可以通过 & 取地址,位于等号左边,所以a是左值。
  • A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。

左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

左值引用和右值引用

引用:引用本质是别名,可以通过引用来修改变量的值,传参时传引用可以避免拷贝。

左值引用

左值引用:能指向左值,不能指向右值的就是左值引用

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

代码中第三行,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是有特例const。

const左值引用

const int &ref_a = 5; // 编译通过

const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const & 作为函数参数的原因之一,这可以按照固定搭配记住。例如:

void push_back (const value_type& val);
...
vec.push_back(5);

如果函数参数没有const , vec.push_back(5) 这样的代码就无法编译通过。

右值引用

右值引用:右值引用专可以指向右值,不能指向左值

右值引用的标志是&& :

int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值

std::move函数

右值引用可以使用std::move可以指向左值

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    int a = 5; // a是个左值
    int &ref_a_left = a; // 左值引用指向左值
    int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
    cout << ref_a_right << endl; // 打印结果:5
    cout << a; // 打印结果:还是5
    return 0;
}
//代码编译时使用C++11新特性
//g++ main.cpp -o main -std=c++11

std::move把一个变量a里的内容移动到另一个变量ref_a_right了吗?

不是!在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。

std::move函数:

  • std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。
  • 其实现等同于一个类型转换:static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升。

右值引用的含义

右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6;
//等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 此时temp也等于6
//两个变量的地址是相同的;&temp,&ref_a是一样的

左值引用、右值引用的本身

被声明出来的左、右值引用都是左值。

因为被声明出的左右值引用是有地址的,都是左值。如下:

// 函数的形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}

如上代码所示int &&ref_a_right = std::move(a); 是一个右值引用,但是ref_a_right是一个左值。

结论:

从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。

右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。

作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

右值引用避免深拷贝

深拷贝可以避免重复析构的问题,可参考帖子深拷贝与浅拷贝定义以及案例说明。但是深拷贝也带来了性能的消耗,这里可以通过移动构造函数避免额外的内存消耗。

#include <iostream>
using namespace std;
class A
        {
        public:
            A() :m_ptr(new int(0)) {
                cout << "constructor A" << endl;
            }
            A(const A& a) :m_ptr(new int(*a.m_ptr)) {
                cout << "copy constructor A" << endl;
            }
            // 移动构造函数,可以浅拷贝
            A(A&& a) :m_ptr(a.m_ptr) {
                a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
                cout << "move constructor A" << endl;
            }
            ~A(){
                cout << "destructor A, m_ptr:" << m_ptr << endl;
                if(m_ptr)
                    delete m_ptr;
            }
        private:
            int* m_ptr;
        };
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
int main()
{
    {
        A a = Get(false); // 正确运行
    }
    cout << "main finish" << endl;
    return 0;
}

运行结果:

建议对比深拷贝与浅拷贝定义以及案例说明中的代码。

  • 可以看到A Get(bool flag),返回参数为A对象,函数中我们返回的a,b是临时变量,所以这里自动会使用移动构造函数通过右值引用接收临时变量。
  • 上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。
  • 从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。
  • 这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
  • 移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。

结论:

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高

C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

(move)移动语义

move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持(也就是需要移动构造函数)。

// 用c++11的右值引用来定义这两个函数
//MyString类的移动构造函数
MyString(MyString&& str) {
  std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
  m_len = str.m_len;
  m_data = str.m_data; //避免了不必要的拷贝
  str.m_len = 0;
  str.m_data = NULL;
}
...
MyString c = std::move(a); // Move Constructor is called! 将左值转为右值

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

forward 完美转发

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

如下面这个简单的例子:

int &&a = 10;
int &&b = a; //错误

这里a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。

这里就可以使用std::forward()完美转发,就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward<int>(a);

这样就是正确的,这里有点像move移动语义的意思,forward是有把左值转为右值的功能,但尽量要以完美转发的角度去理解forward。

上面的是一个简单的完美转发的例子,但完美转发最常见还是处理在模板类中会存在一种“属性变换”现象。

#include <iostream>
using namespace std;
template <class T>
void Print(T &t)
{
    cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
    cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
    Print(t);
    Print(std::move(t));
    Print(std::forward<T>(t));
}
int main()
{
    cout << "-- func(1)" << endl;
    func(1);
    int x = 10;
    int y = 20;
    cout << "-- func(x)" << endl;
    func(x); // x本身是左值
    cout << "-- func(std::forward<int>(y))" << endl;
    func(std::forward<int>(y)); //这里将输入参数转为右值传入了
    return 0;
}

运行结果:

解释一下func(1)结果 :

  1. 由于1是右值,所以未定的引用类型T&&v被一个右值初始化后变成了一个右值引用,但是在func()函数体内部,调用Print(v) 时,v又变成了一个左值(因为在std::forward里它已经变成了一个具名的变量,所以它是一个左值),因此,示例测试结果第一个Print被调用,打印出“L1";
  2. 调用Print(std::move(v))是将v变成一个右值(v本身也是右值),因此输出”R1";
  3. 调用Print(std::forward(v))时,由于std::forward会按参数原来的类型转发,因此,它还是一个右值(这里已经发生了类型推导,所以这里的T&&不是一个未定的引用类型,会调用void Print(T&&t)函数打印“R1”.
目录
相关文章
|
7月前
|
算法 编译器 程序员
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
91 0
|
7月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
42 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
6月前
|
存储 编译器 C++
cpp随笔——浅谈右值引用,移动语义与完美转发
右值引用是C++11引入的关键特性,用于优化资源管理。它分为纯右值(临时对象)和将亡值(即将消失的引用)。右值引用`&&`允许直接访问临时对象,避免拷贝开销。移动语义利用右值引用实现资源转让,提高效率,如在对象构造和赋值时。`std::move`帮助左值转换为右值引用,以利用移动语义。完美转发保持参数的左/右值属性不变,`std::forward`确保在转发时正确处理这些属性。代码示例展示了不同情况下的转发行为。
|
7月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
38 2
|
7月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
45 1
|
6月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
54 0
|
存储 安全 编译器
【C++11新特性】右值引用和移动语义(移动构造,移动赋值)
【C++11新特性】右值引用和移动语义(移动构造,移动赋值)
|
7月前
|
存储 安全 编译器
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
595 0
|
7月前
|
存储 编译器
C++11(左值(引用),右值(引用),移动语义,完美转发)
C++11(左值(引用),右值(引用),移动语义,完美转发)
65 0
|
C++ 容器
C++新特性:右值引用,移动语义,完美转发
C++新特性:右值引用,移动语义,完美转发
66 0