cpp随笔——浅谈右值引用,移动语义与完美转发

简介: 右值引用是C++11引入的关键特性,用于优化资源管理。它分为纯右值(临时对象)和将亡值(即将消失的引用)。右值引用`&&`允许直接访问临时对象,避免拷贝开销。移动语义利用右值引用实现资源转让,提高效率,如在对象构造和赋值时。`std::move`帮助左值转换为右值引用,以利用移动语义。完美转发保持参数的左/右值属性不变,`std::forward`确保在转发时正确处理这些属性。代码示例展示了不同情况下的转发行为。

右值引用

什么是右值

在cpp11中添加了一个新的类型叫做右值引用,记作&&,而在开始今天的正文之前我们先来看一下什么是左值什么是右值:

  • 左值(&):存储在内存中,有明确存储地址的数据
  • 右值(&&):临时对象,可以提供数据(不可取地址访问)

而在cpp11中我们可以将右值分为两种:

  • 纯右值:非引用返回的临时变量,比如运算表达式产生的临时变量,原始字面量以及lambda表达式等
  • 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等

什么是右值引用

右值引用本身就是对右值的引用类型,因为右值是匿名的,所以我们要通过引用来找到它,关于右值引用的使用方法可以参考下面的代码:

#include <iostream>

using namespace std;

class Test
{
   
    public:
     Test()
    {
   
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
   
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj()
{
   
    return Test(); // 返回一个临时对象,如果没有其他引用指向该对象,该对象将被销毁
}

int main()
{
   
    int value=520;  //value是左值
    // int &&a1=value;//error:右值引用无法绑定在左值上
    // Test& a2=getObj();//error:右值无法给左值引用赋值
    Test&& a3=getObj();
    const Test& a4=getObj();//常量左值引用是一个万能引用,可以接受左值,右值,常量左值与常量右值
    return 0;
}

移动语义

讲到这里大家可能有点懵逼,为什么我们要使用右值应用呢?其实道理很简单,如果一个对象拥有像堆区资源,那我们如果想复制它,那么我们就要编写拷贝构造函数与重载赋值函数来实现深拷贝,像下面这样:

#include <iostream>
#include <string.h>

using namespace std;

class Test
{
   
public:
    int* m_data=nullptr;
    void  alloc()
    {
   
        m_data=new int;
        memset(m_data,0,sizeof(int));
    }
    Test() =default;
    Test(const Test& t)
    {
   
        cout<<"调用拷贝构造函数"<<endl;
        if(m_data==nullptr)
        {
   
            alloc();
        }
        memcpy(m_data,t.m_data,sizeof(int));
    }

    Test& operator =(const Test& t)
    {
   
        cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
        if (this == &t)   return *this;                      // 避免自我赋值。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, t.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
        return *this;
    }

    ~Test()
    {
   
        if(m_data!=nullptr)
        {
   
            delete m_data;
            m_data=nullptr;
        }
    }
};

int main()
{
   
    Test t1;
    t1.alloc();
    *t1.m_data=3;
    Test t3(t1);
    Test t2;
    t2=t1;
    return 0;
}

但是每次深拷贝都要进行资源空间申请以及资源拷贝,当我们要拷贝的的对象只不过是一个临时对象,尤其是它即将被销毁的话,这样无疑是耗时耗力不落好,这时候我们就可以使用移动语义来解决这个问题,那什么是移动语义呢?

移动语义是C++编程语言中一种重要的概念,它旨在通过转让而非复制对象的资源来提高程序的性能和效率,特别是在处理大型对象或包含动态分配资源(如内存、文件句柄等)的对象时。移动语义核心思想利用右值引用和特殊成员函数(移动构造函数和移动赋值运算符)来实现资源的所有权转移,而不是复制资源

其实理解起来很简单,相对于左值(类似于int&)是是将引用绑定在一个可以寻址的对象上面进而直接操作,而基于右值引用实现的移动语义一般用于绑定将要被销毁的临时对象上,将该临时对象的资源所有权转移到自己这里,让这一临时对象重获新生。

而要实现移动构造函数就要实现移动构造函数与移动赋值函数,示例代码如下:

#include <iostream>
#include <string.h>

using namespace std;

class Test
{
   
public:
    int* m_data=nullptr;
    void  alloc()
    {
   
        m_data=new int;
        memset(m_data,0,sizeof(int));
    }
    Test() =default;
    Test(const Test& t)
    {
   
        cout<<"调用拷贝构造函数"<<endl;
        if(m_data==nullptr)
        {
   
            alloc();
        }
        memcpy(m_data,t.m_data,sizeof(int));
    }

    Test(Test&& t)
    {
   
        cout<<"调用移动构造函数"<<endl;
        if(m_data!=nullptr) delete m_data;
        m_data=t.m_data;
        t.m_data=nullptr;
    }

    Test& operator=(Test&& t)
    {
   
        cout<<"调用了移动赋值函数。\n";
        if(this==&t)  return *this;
        if(m_data!=nullptr)  delete m_data;
        m_data=t.m_data;
        t.m_data=nullptr;
        return *this;
    }

    Test& operator =(const Test& t)
    {
   
        cout << "调用了赋值函数。\n";                   // 显示自己被调用的日志。
        if (this == &t)   return *this;                      // 避免自我赋值。
        if (m_data == nullptr) alloc();                     // 如果没有分配内存,就分配。
        memcpy(m_data, t.m_data, sizeof(int));    // 把数据从源对象中拷贝过来。
        return *this;
    }

    ~Test()
    {
   
        if(m_data!=nullptr)
        {
   
            delete m_data;
            m_data=nullptr;
        }
    }
};

int main()
{
   
    Test t1;
    t1.alloc();
    Test t2(std::move(t1));
    auto f=[]{
   Test t1;t1.alloc();return t1;};  //lambda表达式,这里是作为临时对象来使用
    Test t3;
    t3=f();
    return 0;
}

拓展: 移动语义的注意点

  • std::move() 函数: 对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
  • 如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
  • C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
  • 移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

完美转发

右值引用是独立于值的,如果我们将右值类型作为函数参数的形参,当函数内部调用其他函数时使用它就会变回左值。cpp11中,在函数模板中,我们可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。而这就是我们所说的完美转发。

C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。而在C++11中提供了std::forward()函数来实现完美转发。

// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精简之后的样子
std::forward<T>(t);

示例代码:

#include <iostream>
#include <cstring>

using namespace std;

template<typename T>

void PrintValue(T& t)
{
   
    cout<<"l-value"<<t<<endl;
}

template<typename T>

void PrintValue(T&& t)
{
   
    cout<<"r-value:"<<t<<endl;
}

template<typename T>

void testForward(T&& t)
{
   
    PrintValue(t);
    PrintValue(std::move(t));
    PrintValue(std::forward<T>(t));
}

int main()
{
   
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));
}

输出为:

root@iZuf6ckztbjhtavfplgp0dZ:~/mylib/cppdemo/cpp11新特性# ./demo2
l-value520
r-value:520
r-value:520
l-value1314
r-value:1314
l-value1314
l-value1314
r-value:1314
r-value:1314
l-value1314
r-value:1314
l-value1314
l-value1314
r-value:1314
r-value:1314
  • testForward(520);函数的形参为未定引用类型T&&,实参为右值,初始化后被推导为一个右值引用
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为`右值
  • testForward(num);函数的形参为未定引用类型T&&,实参为左值,初始化后被推导为一个左值引用
    -printValue(v);实参为左值
    • printValue(move(v));通过move将左值转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为左值引用,最终得到一个左值引用,实参为左值
  • testForward(forward<int>(num));forward的模板类型为int,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值
  • testForward(forward<int&>(num));forward的模板类型为int&,最终会得到一个左值,函数的形参为未定引用类型T&&被左值初始化后得到一个左值引用类型
    • printValue(v);实参为左值
    • printValue(move(v));通过move将左值转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为左值引用,最终得到一个左值,实参为左值
  • testForward(forward<int&&>(num));forward的模板类型为int&&,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型
    • printValue(v);已命名的右值v,编译器会视为左值处理,实参为左值
    • printValue(move(v));已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
    • printValue(forward<T>(v));forward的模板参数为右值引用,最终得到一个右值,实参为右值
相关文章
|
6月前
|
编译器 C语言 C++
右值引用,完美转发,NRVO 和RVO优化(简单易懂详细)
右值引用,完美转发,NRVO 和RVO优化(简单易懂详细)
439 0
|
6月前
|
编译器 C++ 容器
【C++11特性篇】探究【右值引用(移动语义)】是如何大大提高效率?——对比【拷贝构造&左值引用】
【C++11特性篇】探究【右值引用(移动语义)】是如何大大提高效率?——对比【拷贝构造&左值引用】
|
6月前
|
存储 算法 程序员
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
54 0
|
6月前
|
存储 安全 编译器
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
C++ std::move以及右值引用全面解析:从基础到实战,掌握现代C++高效编程
551 0
|
6月前
|
存储 编译器
C++11(左值(引用),右值(引用),移动语义,完美转发)
C++11(左值(引用),右值(引用),移动语义,完美转发)
59 0
|
6月前
|
C++
【C++11特性篇】一文助小白轻松理解 C++中的【左值&左值引用】【右值&右值引用】
【C++11特性篇】一文助小白轻松理解 C++中的【左值&左值引用】【右值&右值引用】
|
6月前
|
编译器 C++
深入理解 C++ 右值引用和移动语义:全面解析
C++11引入了右值引用,它也是C++11最重要的新特性之一。原因在于它解决了C++的一大历史遗留问题,即消除了很多场景下的不必要的额外开销。即使你的代码中并不直接使用右值引用,也可以通过标准库,间接地从这一特性中收益。为了更好地理解该特性带来的优化,以及帮助我们实现更高效的程序,我们有必要了解一下有关右值引用的意义。
94 0
|
C++ 容器
C++新特性:右值引用,移动语义,完美转发
C++新特性:右值引用,移动语义,完美转发
60 0
|
存储 编译器 C++
【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
【C++杂货铺】一文总结C++11新特性:右值引用 | 移动语义 | 完美转发
59 0
|
编译器 C++
【C++】右值引用(极详细版)(二)
在讲右值引用之前,我们要了解什么是右值?那提到右值,就会想到左值,那左值又是什么呢? 我们接下来一起学习!
81 0
【C++】右值引用(极详细版)(二)