[C++11] --- 移动语义和完美转发

Posted Overboom

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++11] --- 移动语义和完美转发相关的知识,希望对你有一定的参考价值。

1 预备知识

在介绍std::move和std::forward之前,先来了解一下这几种函数:

  • 拷贝构造函数(copy constructor)
  • 移动构造函数(move constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动赋值运算符(move-assignment operator)
#include <iostream>
#include <cstdlib>
#include <assert.h>

class CTest

public:
	CTest()
	
		std::cout << "Construct " << std::endl;
		this->v = (int *)malloc(sizeof(int));
		*this->v = 10;
	
	CTest(const CTest &other)
	
		std::cout << "copy construct " << std::endl;
		assert(this->v == nullptr);
		this->v = (int *)malloc(sizeof(int));
		*(this->v) = *(other.v) + 100;
	
	CTest(CTest &&other)
	
		std::cout << "move contruct" << std::endl;
		assert(this->v == nullptr);
		this->v = other.v;
		other.v = nullptr;
	


	CTest &operator=(const CTest &other)
	
		std::cout << "copy assign " << this->v << " " << other.v << std::endl;
		*(this->v) = *(other.v);
		return *this;
	
	CTest &operator=(CTest &&other)
	
		std::cout << "move assign" << std::endl;
		if (this->v)
		
			std::cout << "free " << this->v << "move "
				<< other.v << std::endl;
			free(this->v);
		
		this->v = other.v;
		other.v = nullptr;
		return *this;
	

	~CTest()
	
		if (!this->v)
			return;
		std::cout << "free " << this->v << std::endl;
		free(this->v);
	
	void print()
	
		std::cout << "hello " << this->v << " " << *(this->v) << std::endl;
	
private:
	int *v;
;


int main()

	// 拷贝构造
	CTest a;
	a.print();

	CTest a1 = a;
	a1.print();

	CTest a2(a);
	a2.print();

	CTest a3 a ;
	a3.print();

	// 拷贝赋值
	CTest b;
	b.print();

	CTest b1;
	b1 = b;
	b1.print();

	// 移动构造
	CTest c;
	c.print();
	// 如果没有实现移动构造, 会默认调用拷贝构造
	CTest c1 = std::move(c);


	//如果没有试下移动赋值,会默认调用拷贝赋值
	CTest h3, h4;
	h4 = std::move(h3);

	return 0;

  • 拷贝构造函数和拷贝赋值函数区别?
    拷贝构造函数和拷贝赋值函数都是,使用a对象去初始化另一个a1对象
    若a1对象未实例化,则调用拷贝构造函数
    若a1对象已初始化,则调用拷贝赋值函数
    其中拷贝构造函数有三种调用方式,效果都都一样

     CTest a1 = a;
     a1.print();
    
     CTest a2(a);
     a2.print();
    
     CTest a3 a ;
     a3.print();
    
  • 拷贝构造函数和移动构造函数的区别
    函数形参类型不同:
    拷贝构造函数形参类型为 &(左值引用)
    移动构造函数形参类型为 &&(右值引用)
    函数中操作不同:
    拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。有一次拷贝对象的开销
    移动构造函数就是为了解决这个拷贝开销而产生的。移动构造函数干的是浅拷贝,移动构造函数首先将传递参数的内存地址空间接管,然后将内部所有指针设置为nullptr,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。

  • 移动构造函数和移动赋值函数的区别?
    同拷贝构造函数和拷贝赋值函数的区别

  • 如果类中没有实现移动构造函数, 当使用std::move使用移动构造函数时,会默认调用拷贝构造函数

  • -如果类中没有实现移动赋值函数, 当使用std::move使用移动赋值函数时,会默认调用拷贝赋值函数

2 几个概念

在目前编程语言中,一般变量类型可以分类为

  • 值类型
  • 指针类型
  • 引用类型

目前很多语言中只有引用类型,比如java出了基本类型以外都是引用类型,但是C+ + 都用这三种类型。C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。在介绍右值引用类型之前先要了解几个基本概念。

2.1 左值 右值 将亡值

左值( loactor value )是指存储在内存中、有明确存储地址(可取地址)的数据;
右值( read value )和左值相反,一般指的是没有对应存储单元的值(寄存器中的立即数,中间结果等),例如一个常量,或者表达式计算的临时变量;

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):
纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。

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

2.2 左值引用和右值引用

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

3 移动语义std::move

右值引用的作用是减少内存开销,优化程序性能
在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
std::move的作用就是进行无条件转化,任何的左值/右值通过std::move都转化为右值引用。

上菜,看一段代码,使用右值引用减少内存开销 [代码摘抄自https://zhuanlan.zhihu.com/p/469607144]

#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)(这种采用右值引用作为参数的构造函数又称作移动构造函数),此时不需要额外的拷贝操作,也不需要新分配内存。

4 完美转发std::forward

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

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

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

上菜,程序段摘抄自 [https://subingwen.cn/cpp/move-forward/#2-forward]

#include <iostream>

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 && v)

    printValue(v);
    printValue(std::move(v));
    printValue(std::forward<T>(v));
    cout << endl;


int main()

    testForward(520);

    return 0;


//----------------output--------------------
//l-value: 520
//r-value: 520
//r-value: 520

程序解析:

  1. printValue(520); // l-value: 520
    520明明是右值,为什么编译器会识别成左值?
    原来是在程序的执行过程中,对于引用的传递实际上会有额外的隐式的转化,一个右值引用参数经过函数的调用转发可能会转化成左值引用,但这就不是我们希望看到的结果。
  2. printValue(std::move(520)); //r-value: 520
    已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值,很好理解
  3. printValue(std::forward(v)); //r-value: 520
    forward 的模板参数T为右值引用,入参v为右值引用,折叠规则,最终得到一个右值,实参为 ``右值`,也很好理解

以上是关于[C++11] --- 移动语义和完美转发的主要内容,如果未能解决你的问题,请参考以下文章

[C++11] --- 移动语义和完美转发

[C++11] --- 移动语义和完美转发

[c++11]右值引用移动语义和完美转发

C++11:移动语义Move Semantics和完美转发Perfect Forwarding

C++11:移动语义Move Semantics和完美转发Perfect Forwarding

C++11:移动语义Move Semantics和完美转发Perfect Forwarding