[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
程序解析:
- printValue(520); // l-value: 520
520明明是右值,为什么编译器会识别成左值?
原来是在程序的执行过程中,对于引用的传递实际上会有额外的隐式的转化,一个右值引用参数经过函数的调用转发可能会转化成左值引用,但这就不是我们希望看到的结果。 - printValue(std::move(520)); //r-value: 520
已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值,很好理解 - printValue(std::forward(v)); //r-value: 520
forward 的模板参数T为右值引用,入参v为右值引用,折叠规则,最终得到一个右值,实参为 ``右值`,也很好理解
以上是关于[C++11] --- 移动语义和完美转发的主要内容,如果未能解决你的问题,请参考以下文章
C++11:移动语义Move Semantics和完美转发Perfect Forwarding