C++中的std::move、移动构造函数、右值、函数返回值联系
前言
本文简要概述std::move、移动构造函数、右值、函数返回值的概念及其相关联系。
一、std::move
在C++中std::move的作用很简单,一言以蔽之就是将左值变成右值。
常见用法:
int a = 10;
auto&& b = std::move(a);
class test{
public:
int a = 0;
int b = 0;
};
test c;
auto&& d = std::move(c);
二、左值、右值
左值:简单来说,就是普通的变量,正常使用的值。一言以蔽之,左值可以重复使用,并反复进行赋值拷贝。
右值:即将消亡的值,简单来说,右值就是即将消亡的值。一言以蔽之,右值只能使用一次。当使用右值引用即将消亡的右值时,该值就变成了左值,即可以被重复使用了。
引用:变量的别名,本质上就是不可为空的指针。
三、移动构造函数
移动构造函数简单来说就是接收一个右值的构造函数。
具体如下:
class test{
public:
test(test&& tmp) {
this->a = tmp;
this->b = tmp;
}
int a = 0;
int b = 0;
};
之所以叫移动构造函数,显然该构造函数与std::move有关。简单来说,就是使用将亡对象构造对象。
一般对于包含指针的对象来说,移动构造应当执行浅拷贝,即直接将指针值赋予对应的指针元素,并应当将被拷贝的指针设为空指针,避免二次释放内存。普通的拷贝构造函数执行深拷贝,重新申请内存,并将被拷贝的内存中的值赋值过来。
class test{
public:
test(){ this->a = (int*)malloc(sizeof(int)*100); }
test(test&& other){
this->a = other.a;
other.a = nullptr;
}
test(const test& other){
this->a = (int*)malloc(sizeof(int)*100);
memcpy(this->a, other.a, sizeof(int)*100);
}
~test(){
if(a){
free(a);
}
}
private:
int *a;
};
三、函数返回值
1 debug模式
很显然函数返回值是一个将亡值。在接收函数返回值的时候会执行一次拷贝构造或者移动函数。但是C++编译器会非常智能,当存在拷贝构造函数的时候会自动执行拷贝构造函数,当存在移动构造函数的时候会默认先执行移动构造函数。值得注意的是,当存在接收目标的时候,编译器会将结果直接调用移动构造函数或者拷贝构造函数到目标位置,且不会创建临时变量。这与很多讲解C++函数调用返回的过程显然是不同的。
class Tet {
public:
Tet(int id, int v1, int v2, int v3, int v4) {
this->id = id;
this->v1 = v1;
this->v2 = v2;
this->v3 = v3;
this->v4 = v4;
}
Tet(const Tet& other) noexcept{
this->id = other.id;
this->v1 = other.v1;
this->v2 = other.v2;
this->v3 = other.v3;
this->v4 = other.v4;
std::cout << "copy constructor" << std::endl;
}
/* Tet(Tet&& tmp) noexcept {
this->id = tmp.id;
this->v1 = tmp.v1;
this->v2 = tmp.v2;
this->v3 = tmp.v3;
this->v4 = tmp.v4;
std::cout << "move constructor" << std::endl;
} */
int id;
int v1;
int v2;
int v3;
int v4;
};
Tet Test(int a) {
Tet tet(1, 2, 3, 4, 5);
tet.id += a;
tet.v1 += a;
tet.v2 += a;
tet.v3 += a;
tet.v4 += a;
for (int i = 0; i < a; i++) {
tet.id *= (i + 1) * 2;
tet.v1 *= (i + 2);
tet.v2 *= (i + 3);
tet.v3 *= (i + 4);
tet.v4 *= (i + 5);
}
for (int i = 1; i < a; i++) {
tet.id += (i) * 3;
tet.v1 += (i);
tet.v2 += (i + 3);
tet.v3 += (i + 4);
tet.v4 += (i + 5);
}
return tet;
}
int main()
{
int b;
std::cin >> b;
auto&& c = Test(b + 1);
Tet ret = Test(b);
std::cout << ret.id << " " << ret.v1 << " " << ret.v2 << " " << ret.v3 << " " << ret.v4 << std::endl;
std::cout << c.id << " " << c.v1 << " " << c.v2 << " " << c.v3 << " " << c.v4 << std::endl;
return 0;
}
当取消掉移动构造函数注释以后:
2 release模式
在release模式下会出现非常非常神奇的情况,强大的编译器将其变成了一种另类的构造函数,这真的是非常非常神奇。因为整个过程中完全没有出现调用拷贝构造或者移动构造的情况。当然,如果是更复杂的函数,可能情况会出现不同。
以上结果完全是在Visual Studio 2022情况下产生的,在GCC中基本也属于相同模式。
以下是汇编代码,显然两者都采用了直接构造的技术:
在这种模式下我们可以发现以下两种写法基本属于等价的:
#写法一
Tet Test(int a);
#写法二
void Test(Tet& output, int a);
总结
简单来说,std::move唯一的作用是产生右值,将左值变成右值。左值是可以多次使用的值,右值是即将消亡的值,只能使用一次,无论是左值引用还是右值引用,其代表的都是左值,可以被多次使用。右值可以被用来执行移动构造。对于函数调用,根据规范,必然会产生一次拷贝构造或者移动构造,一般会默认执行移动构造。而release模式下,可能会将其优化成一种另类的构造函数。