首页 > 编程语言 >[C++特性]对std::move和std::forward的理解

[C++特性]对std::move和std::forward的理解

时间:2023-05-10 09:33:05浏览次数:43  
标签:std 右值 move 左值 C++ 引用 array size

左值、右值、左值引用以及右值引用

std::move和std::forward这两个API主要服务于左值引用和右值引用的转化和转发,因此再了解这两个API之前,需要先弄清楚这几个概念。

左值:一般指的是在内存中有对应的存储单元的值,最常见的就是程序中创建的变量

右值:和左值相反,一般指的是没有对应存储单元的值(寄存器中的立即数,中间结果等),例如一个常量,或者表达式计算的临时变量

int x = 10 

int y = 20

int z = x +

//x, y , z 是左值

//10 , 20,x + y 是右值,因为它们在完成赋值操作后即消失,没有占用任何资源

左值引用:C++中采用 &对变量进行引用,这种常规的引用就是左值引用

右值引用:这个概念实际上不是说对上述的右值进行引用(因为右值本身也没有对应的存储单元),右值引用实际上只是一个逻辑上的概念,最大的作用就是让一个左值达到类似右值的效果(下面程序举例),让变量之间的转移更符合“语义上的转移”,以减少转移之间多次拷贝的开销。右值引用符号是&&。

例如,对于以下程序,我们要将字符串放到vector中,且我们后续的代码中不再用到x:

std::vector<std::string> vec;

std::string x = "abcd";

vec.push_back(x);

std::cout<<"x: "<<x<<"\n";

std::cout<<"vector: "<< vec[0]<<"\n";

 

//-------------output------------------

// x: abcd

// vector: abcd

该程序在真正执行的过程中,实际上是复制了一份字符串x,将其放在vector中,这其中多了一个拷贝的开销和内存上的开销。但如果x以及没有作用了,我们希望做到的是 真正的转移,即x指向的字符串移动到vector中,不需要额外的内存开销和拷贝开销。因此我们希望让变量 x传入到push_back 表现的像一个右值 ,这个时候就体现右值引用的作用,只需要将x的右值引用传入就可以。

std::move

前面提到了右值引用的主要作用是减少不必要的拷贝开销和内存开销。而std::move的作用就是进行无条件转化,任何的左值/右值通过std::move都转化为右值引用。将上面的程序改写成右值引用的方式

std::vector<std::string> vec;

std::string x = "abcd";

vec.push_back(std::move(x));

std::cout<<"x: "<<x<<"\n";

std::cout<<"vector: "<< vec[0]<<"\n";

//-------------output------------------

// x: 

// vector: abcd

可以看到,完成`push_back`后x是空的。

使用场景

对于一个值(比如数组、字符串、对象等)如果在执行某个操作后不再使用,那么这个值就叫做将亡值(Expiring Value),因此对于本次操作我们就没必要对该值进行额外的拷贝操作,而是希望直接转移,尽可能减少额外的拷贝开销,操作后该值也不再占用额外的资源,此时就可以使用std::move。

举个例子

#include <iostream>

#include <vector>

#include <string>

 

class A {

  public:

    A(){}

    A(size_t size): size(size), array((int*) malloc(size)) {

        std::cout 

          << "create Array,memory at: "  

          << array << std::endl;

        

    }

    ~A() {

        free(array);

    }

    A(A &&a) : array(a.array), size(a.size) {

        a.array = nullptr;

        std::cout 

          << "Array moved, memory at: " 

          << array 

          << std::endl;

    }

    A(A &a) : size(a.size) {

        array = (int*) malloc(a.size);

        for(int i = 0;i < a.size;i++)

            array[i] = a.array[i];

        std::cout 

          << "Array copied, memory at: " 

          << array << std::endl;

    }

    size_t size;

    int *array;

};

int main() {

    std::vector<A> vec;

    A a = A(10);

    vec.push_back(a);   

    return 0;   

}

 

//----------------output--------------------

// create Array,memory at: 0x600002a28030 // A a = A(10); 调用了 构造函数A(size_t size){}

// Array copied, memory at: 0x600002a28050 //vec push的时候拷贝一份,调用构造函数A(A &a){}

从输出可以看到,每次进行push_back的时候,会重新创建一个对象,调用了左值引用A(A &a) : size(a.size)对应的构造函数,将对象中的数组重新深拷贝一份,如果对象占用内存大,并且该对象此时已经是一个将亡值,那么这样带来了许多不必要的开销,降低了程序的性能。这个时候就可以用右值引用进行优化,避免拷贝的开销

int main () {

    std::vector<A> vec;

    A a = A(10);

    vec.push_back(std::move(a));   

    return 0;   

}

 

//----------------output--------------------

// create Array,memory at: 0x600003a84030

// Array moved, memory at: 0x600003a84030

可以看到,这个时候虽然也重新创建了一个对象,但是调用的是这个构造函数A(A &&a) : array(a.array), size(a.size)(这种采用右值引用作为参数的构造函数又称作移动构造函数),此时不需要额外的拷贝操作,也不需要新分配内存。

std::forward

std::forward的作用是完美转发,如果传递的是左值转发的就是左值引用,传递的是右值转发的就是右值引用。

在具体介绍std::forward之前,需要先了解C++的引用折叠规则,对于一个值引用的引用最终都会被折叠成左值引用或者右值引用。

T& & -> T& (对左值引用的左值引用是左值引用)

T& && -> T& (对左值引用的右值引用是左值引用)

T&& & ->T& (对右值引用的左值引用是左值引用)

T&& && ->T&& (对右值引用的右值引用是右值引用)

只有对于右值引用的右值引用折叠完还是右值引用,其他都会被折叠成左值引用,根据折叠规则,可以构造出一个通用引用。

#include<iostream>

template <typename T>

void foo(T&& param){

   if(std::is_rvalue_reference<decltype(param)>::value)

        std::cout<<"rvalue reference\n";

    else std::cout<<"lvalue reference\n";

}

int main(){

    int a = 0;

    foo(a);  

    foo(std::move(a)); 

    return 0;

}

 

//------------output----------

// lvalue reference

// rvalue reference

foo(a) ,T就是int &,则param的类型为T&&->int & &&->int &

foo(std::move(a)),std::move转成右值引用,那么T就是int&&,则param的类型为T &&->int && &&->int &&

前面提到的std::move可以减少不必要的拷贝开销,可以提高程序的效率,但是std::forward的作用是转发,左值引用转发成左值引用,右值引用还是右值引用,刚开始一直想不通这个API的意义到底是什么?

原来是在程序的执行过程中,对于引用的传递实际上会有额外的隐式的转化,一个右值引用参数经过函数的调用转发可能会转化成左值引用,但这就不是我们希望看到的结果。

在上面的程序上进行修改

#include <iostream>

#include <vector>

#include <string>

 

class A {

  public:

    A(){}

    A(size_t size): size(size), array((int*) malloc(size)) {

        std::cout 

          << "create Array,memory at: "  

          << array << std::endl;

        

    }

    ~A() {

        free(array);

    }

    A(A &&a) : array(a.array), size(a.size) {

        a.array = nullptr;

        std::cout 

          << "Array moved, memory at: " 

          << array 

          << std::endl;

    }

    A(A &a) : size(a.size) {

        array = (int*) malloc(a.size);

        for(int i = 0;i < a.size;i++)

            array[i] = a.array[i];

        std::cout 

          << "Array copied, memory at: " 

          << array << std::endl;

    }

    size_t size;

    int *array;

};

template<typename T>

void warp(T&& param) {

    if(std::is_rvalue_reference<decltype(param)>::value){

        std::cout<<"param is rvalue reference\n";

    }

    else std::cout<<"param is lvalue reference\n";

    A y = A(param);

    A z = A(std::forward<T>(param));

}

int main(){

    A a = A(100);

    warp(std::move(a));

    return 0;   

}

 

//----------------output----------------

// create Array,memory at: 0x600002e60000 //main函数中,A a = A(100);调用构造函数

// param is rvalue reference //使用了std::move,根据引用折叠规则,param是一个右值引用

// Array copied, memory at: 0x600002e60070 // A y = A(param); 可以看到调用的是拷贝的构造函数

// Array moved, memory at: 0x600002e60000。// A z = A(std::forward<T>(param)); 调用了移动构造函数

从程序的输出就可以看到,当一个右值引用再进行转发的时候,没使用std::forward进行二次转发的时候,实际上是会被隐式的转换,转发成一个左值引用,从而调用不符合期待的构造函数,带来额外的开销,所以std::forward的一个重要作用就是完美转发,确保转发过程中引用的类型不发生任何改变,左值引用转发后一定还是左值引用,右值引用转发后一定还是右值引用

总结

如果想要更深入和正确的理解这些概念还是需要看一些官方的资料以及API的实现,通过理解std::move和std::forward源码,感觉这两个API实际上并没有真正的move或者forward任何的数据或资源,真正做的事情只是对数据的类型进行强制的cast,以达到逻辑上(语义上)的区分值的左值引用和右值引用,但实际上要做的事情还需要额外的实现,例如上面程序中使用右值引用区分开了普通的构造函数和移动构造函数以减少拷贝的开销,但是实际上只是起到了区分以调用不同的构造函数,真正减少拷贝开销的还是函数中的程序实现过程,当然你也可以在移动构造函数里面也进行深拷贝,那么额外的开销还是存在的,和普通的构造函数没有什么区别,你也可以在普通构造函数里面不进行深拷贝,只赋值指针,那是不是也可以说成没有额外的开销?甚至即使没有这个右值引用的构造函数,我们实际上也可以引入额外的变量,以区分不同情况下到底是要进行深拷贝还是浅拷贝,也能得到类似的效果,但是从整个程序的逻辑和语义的角度上看,这样的实现就显得有点混乱("高级语言能实现的,用汇编也都能实现"

因此我个人觉得,右值引用、std::move、std::forward是服务于一些特定场景下值的转移和转发,引入这些概念能让程序的语义更加通顺,逻辑更清晰,同时还能避免一些不必要的开销,得到一些性能上的提升。

参考资料

std::move(expr)和std::forward(expr)参数推导的疑问?

浅谈std::move和std::forward原理_爱拼才会赢-CSDN博客_std::forward作用

https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues

C++ 左值、右值与右值引用 | 曜彤.手记

 

 

from:https://zhuanlan.zhihu.com/p/469607144

标签:std,右值,move,左值,C++,引用,array,size
From: https://www.cnblogs.com/im18620660608/p/17387025.html

相关文章

  • 每日打卡c++中vector容器使用
    首先头文件#include<vector>for_each一种算法需要头文件#include<algorithm>标准算法头文件vector<int>::iterator迭代器,可以当指针用。基本格式vector<数据类型>名称;数据类型可以是类。例子#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;cla......
  • c++打卡第二十一天
    一、爱因斯坦的数学题1、问题描述2、设计思路。①、输入N,从1到n遍历。②、寻找满足上述条件的数,得到符合条件的个数加一并打印出这个数。3、流程图4、代码实现#include<iostream>usingnamespacestd;intmain(){intN;intflag=1;intcount;whi......
  • 现代 C++:Lambda 表达式
    Lambda表达式(LambdaExpression)是C++11引入的一个“语法糖”,可以方便快捷地创建一个“函数对象”。从C++11开始,C++有三种方式可以创建/传递一个可以被调用的对象:函数指针仿函数(Functor)Lambda表达式函数指针函数指针是从C语言老祖宗继承下来的东西,比较原始,功能也比......
  • C++ Lambda表达式的完整介绍
    c++在c++11标准中引入了lambda表达式,一般用于定义匿名函数,使得代码更加灵活简洁。lambda表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是它的定义方式更简洁,并且可以在函数内部定义。什么是Lambda表达式最常见的lambda的表达式写法如下autoplus=[](intv1,int......
  • C++11 lambda表达式精讲
    lambda表达式是C++11最重要也最常用的一个特性之一,C#3.5和Java8中就引入了lambda表达式。 lambda来源于函数式编程的概念,也是现代编程语言的一个特点。C++11这次终于把lambda加进来了。 lambda表达式有如下优点:声明式编程风格:就地匿名定义目标函数或函数对......
  • 1009 说反话(C++)
    一、问题描述:给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。输入格式:测试输入包含一个测试用例,在一行内给出总长度不超过80的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用1个空格分开,输入保证句子末尾没有......
  • RustDesk 远程桌面
    RustDesk是一款开源远程桌面软件。有云服务器的话,可以几分钟就搭一个,本文是搭建的记录。自建服务器下载服务器程序,#上传进服务器,假设其IP为`x.x.x.x`[email protected]:登录进服务器:#解压unziprustdesk-server-linux-amd64.zip#......
  • c++打卡练习(23)
    亲密数如果整数A的全部因子(包括1,不包括A本身)之和等于B;且整数B的全部因子(包括1,不包括B本身)之和等于A,则将整数A和B称为亲密数。求3000以内的全部亲密数。流程图:伪代码:源代码:#include<iostream>usingnamespacestd;intmain(){ inta,i,b,n; printf("Therearefollowing......
  • C++异常和错误处理机制:如何使您的程序更加稳定和可靠
    在C++编程中,异常处理和错误处理机制是非常重要的。它们可以帮助程序员有效地处理运行时错误和异常情况。本文将介绍C++中的异常处理和错误处理机制。什么是异常处理?异常处理是指在程序执行过程中发生异常或错误时,程序能够捕获并处理这些异常或错误的机制。例如,当程序试图访问......
  • C++异常和错误处理机制:如何使您的程序更加稳定和可靠
    在C++编程中,异常处理和错误处理机制是非常重要的。它们可以帮助程序员有效地处理运行时错误和异常情况。本文将介绍C++中的异常处理和错误处理机制。什么是异常处理?异常处理是指在程序执行过程中发生异常或错误时,程序能够捕获并处理这些异常或错误的机制。例如,当程序试图访问一......