左值和右值
代码例子根据这篇博客修改而来 move的描述引用这篇博客 左值是一般是出现在等号左边的,是可以取地址的。右值一般出现在等号右边,不可以取地址。非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。 C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:
类型 && 引用名 = 右值表达式; 例如
#include <iostream>
using namespace std;
int main()
{
int num = 10;
int && a = 10;
a = 100;
cout << a << endl;
system("pause");
return 0;
}
std::move
先看一个使用的例子
int &&rr1 = 42; //正确:字面常量的右值 int &&rr2 = rr1; //错误:表达式rr1是左值
为了能避免此类错误,标准库引入std::move()
int &&rr2 = std::move(rr1); //正确
因为move的作用就是强制类型转换,将传入的左值rr1强转为右值,除此之外没有做任何事。
例子
#include <string>
#include <vector>
#include <iostream>
#include <variant>
using namespace std;
class A {
public:
int x;
A(int x) : x(x)
{
cout << "Constructor" << endl;
}
A(A& a) : x(a.x)
{
cout << "Copy Constructor" << endl;
}
A& operator=(A& a)
{
x = a.x;
cout << "Copy Assignment operator" << endl;
return *this;
}
A(A&& a) : x(a.x)
{
cout << "Move Constructor" << endl;
}
A& operator=(A&& a)
{
x = a.x;
cout << "Move Assignment operator" << endl;
return *this;
}
};
int main()
{
A a(1);
A e(2);
e = A(a);
A d = move(e);
system("pause");
return 0;
}
输出为:
Constructor Constructor Copy Constructor Move Assignment operator Move Constructor
首先要清楚为什么要自定义移动构造(右值构造)和移动赋值(右值赋值)这两个函数? 这个问题在文章最后回答。 这里面移动构造对应以前学过的拷贝构造函数,但是移动构造传入的是右值,即这个函数的参数类型是A&& a,而不是拷贝构造函数的A& a。 这里面移动赋值对应以前学过的赋值运算符Copy Assignment operator,它们最大的不同也是传入的参数不同,移动赋值是A&& a,赋值运算是A& a。 上面这段代码首先用构造函数调用a和e两个对象。在e = A(a)中,首先 A(a)会调拷贝构造Copy Constructor,然后由于e是存在的对象,则调用移动赋值。 对于A d = move(e) 其实它就是A d(move(e)) ,move(e)将e变为右值,然后在对新对象d构造时,发现传入的参数是A && 即右值,便会调用移动构造,而不是普通的构造函数。
再看一个例子
#include <string>
#include <vector>
#include <iostream>
#include <variant>
using namespace std;
class A {
public:
int x;
A(int x) : x(x)
{
cout << "Constructor" << endl;
}
A(A& a) : x(a.x)
{
cout << "Copy Constructor" << endl;
}
A& operator=(A& a)
{
x = a.x;
cout << "Copy Assignment operator" << endl;
return *this;
}
A(A&& a) : x(a.x)
{
a.x = 0;
cout << "Move Constructor" << endl;
}
A& operator=(A&& a)
{
x = a.x;
cout << "Move Assignment operator" << endl;
return *this;
}
};
int main()
{
A* a = new A(1);
A* b = new A(1);
A* c = new A(1);
shared_ptr<A> p1(a);
shared_ptr<A> p2(b);
shared_ptr<A> p3(c);
cout << "before move " << p1.get() << endl;
A res1 = std::move(*p1);
cout << "after move " << p1.get() << endl;
cout << "before move " << p2.get() << endl;
shared_ptr<A>&& res2 = std::move(p2);
cout << "after move " << p2.get() << endl;
cout << "before move " << p3.get() << endl;
shared_ptr<A> res3= std::move(p3);
cout << "after move " << p3.get() << endl;
system("pause");
return 0;
}
输出结果
Constructor Constructor Constructor before move 009ED6B0 Move Constructor after move 009ED6B0 before move 009ED7C8 after move 009ED7C8 before move 009ED7F8 after move 00000000
在A res1 = std::move(p1);执行后,会调用移动构造Move Constructor,因为p1实际就是对象a,这里把对象从左值转到了右值,于是调用了移动构造,跟上一个例子的A d = move(e);是一样的 但是由于我们对Move Constructor将进行了小小的修改,将等号右边的a的x设为0,所以执行后a 的x为0。即修改成这样后才符合移动的语义,毕竟移动嘛,就应该直接把右边的值“掏空”转移到等号左边。 所以以后构建一个新类的时候,如果需要创建这移动构造或者移动赋值都需要将类的基本类型(如果是int就设为0),指针置空,复杂类型继续调用复杂类型的移动构造或者移动赋值。保证移动之后,该对象什么都没有了,并且移动的过程最好是直接进行指针的转移,这样能保证移动的高效性,而不会由于资源的拷贝而导致多余的计算操作。
shared_ptr&& res2 = std::move(p2);执行后,res2本身是一个右值,当你赋值的时候,res2本身就是p2的别名。
shared_ptr res3= std::move(p3);执行后,res3以右值构造函数接收参数,所以stl根据右值规则,拿走了p3的值,并且制空p3. 这个置空 是因为这个智能指针类在右值构造函数自己实现了资源的转移操作 所以才置空的。
具体如果想看具体实现移动构造和移动赋值的类,可以参考开头的超链接里的,这里我直接粘贴到下面,当然更建议的是看官方类的实现方法,比如智能指针类。
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class String
{
public:
char* str;
String() : str(new char[1])
{
str[0] = 0;
}
String(const char* s)
{
cout << "调用构造函数" << endl;
int len = strlen(s) + 1;
str = new char[len];
strcpy_s(str, len, s);
}
String(const String & s)
{
cout << "调用复制构造函数" << endl;
int len = strlen(s.str) + 1;
str = new char[len];
strcpy_s(str, len, s.str);
}
String & operator = (const String & s)
{
cout << "调用复制赋值运算符" << endl;
if (str != s.str)
{
delete[] str;
int len = strlen(s.str) + 1;
str = new char[len];
strcpy_s(str, len, s.str);
}
return *this;
}
String(String && s) : str(s.str)
{
cout << "调用移动构造函数" << endl;
s.str = new char[1];
s.str[0] = 0;
}
String & operator = (String && s)
{
cout << "调用移动赋值运算符" << endl;
if (str != s.str)
{
str = s.str;
s.str = new char[1];
s.str[0] = 0;
}
return *this;
}
~String()
{
delete[] str;
}
};
template <class T>
void MoveSwap(T & a, T & b)
{
T tmp = move(a);
a = move(b);
b = move(tmp);
}
template <class T>
void Swap(T & a, T & b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
String s;
s = String("this");
cout << "print " << s.str << endl;
String s1 = "hello", s2 = "world";
Swap(s1, s2);
cout << "print " << s2.str << endl;
system("pause");
return 0;
}
回答上面的那个问题: 所以这两个移动函数的目的是,将右值的所有资源直接转移到左边,而不是拷贝和赋值中,对对象等资源的深度复制,这些复制都是需要花费计算资源和时间的。因为我们已经用move将左值转换成了右值,根本不需要关心右值以后还需要用到,因为右值的定义就是一个临时对象。所以你可以在类实现这两个移动函数的时候,直接粗暴的将资源全部转移到左值中,对于类中的资源最好不要拷贝,直接进行指针所有权的转移。
|