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

右值引用

什么是右值

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

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

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

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

什么是右值引用

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

cpp 复制代码
#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;
}

移动语义

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

cpp 复制代码
#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&)是是将引用绑定在一个可以寻址的对象上面进而直接操作,而基于右值引用实现的移动语义一般用于绑定将要被销毁的临时对象上,将该临时对象的资源所有权转移到自己这里,让这一临时对象重获新生。

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

cpp 复制代码
#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()函数来实现完美转发。

cpp 复制代码
// 函数原型
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);

示例代码:

cpp 复制代码
#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));
}

输出为:

cpp 复制代码
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的模板参数为右值引用,最终得到一个右值,实参为右值
相关推荐
姑苏风1 分钟前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
Nu11PointerException1 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程1 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye2 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang