【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符

右值引用与TArray、TString

参考文献

引子

前文讲到一个点:

1
2
3
4
5
// 差 - 返回常量数组
const TArray<FString> GetSomeArray();

// 优 - 返回常量数组的引用
const TArray<FString>& GetSomeArray();

那为啥要这么写呢?这其中涉及到了右值引用这个知识点。

右值引用要解决啥问题

我们知道,C++中把值对象(包括栈上面分配的任何内存:基元类型或任何不是new出来的类实例)作为右值赋值给另外一个左值的时候,会触发复制。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test
{
public:
int32 Value;

/** 拷贝构造函数 */
Test(const Test& Another)
{
Value = Another.Value;
}
}

Test Foo()
{
Test Ret = Test();
return Ret;
}

int main()
{
Test t = Foo();
}

在上面这段代码中,要注意两点:

  1. 实际上Foo()真正返回到main函数中的对象和Foo中的Ret不是同一个,而是经过值复制得来的。
  2. 拿到Foo()的返回值之后,还需要执行拷贝构造函数,才能得到对象

这就造成了一个没有必要的消耗,也就是多了一次值复制的过程。

按照知乎帖子的神比喻来说:

如何将大象从一台冰箱转移到另一台冰箱?普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。

2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。

等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?

其实这样子设计也可以理解,毕竟Ret是分配于栈上的一个临时变量,当超出Foo()的定义域的时候就应该消失,此时外界还想要获取到该值,就应该通过值复制拷贝一份出去。

所以我们要解决这个不必要的值复制的问题。

怎么解决呢?临时变量不要被回收就好了

既然临时变量要被回收,那么就意味着它不能再被其他任何地方用到。那么,在法律上属于被遗弃品,我们拾取了它,转移它的所有权即可。

所谓的右值引用,实质上就是一个「转移所有权」的过程,它把一个右值的所有权赋予了一个左值。(相对的,左值引用的意义比较简单,就是相当于给一个左值起了别名。)

左值右值傻傻分不清

看了那么多博客,这里直接说个结论吧。

所谓左值右值,乍看一下就是等号的左右边的值。其实不然,左右值的本质区别在于:

  • 对于左值,指的是变量的存储位置
  • 对于右值,指的是变量的值;可以是一个常量,也可以是函数返回的变量

右值引用

前面简单地提到了右值引用。所谓右值引用,可以简单地理解为「控制权转移」。

有了右值引用,C++世界变得有效率得多了。当使用右值赋值给左值的时候,可以直接将右值的控制权移交给左值,而不是先把右值复制一遍,再交给左值。

在右值引用之前,如果将参数传进来,一般用的是const左值引用,如下:

1
2
3
4
5
6
7
8
9
void Foo(const string& str);

void main()
{
string a = "a";
Foo(a); // 没有复制,没有性能损耗

Foo("rvalue"); // 会产生一次转换
}

如果我们编写了右值引用,那就会直接采用右值引用的版本

1
void Foo(string&& str);

为了避免每次都要定义两个版本的函数,我们可以只定义一个右值引用的版本,然后采用std::move()把左值引用都变成右值引用。

右值引用代表着原拥有者放弃了所有权,由于一般都是临时变量,所以放弃了所有权也就放弃了,随便别人拿到所有权后咋整都行。

再举个例子。在右值引用和左值引用函数同时存在(重写了)时,如果传进来的是右值,会优先使用右值引用版本的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Test
{
public:
// 可能是一个大数组
int* Data;

Test()
: Data(new int[10000000])
{}

Test(const Test& Another)
{
/**肯定不能够将Another的Data的地址赋值给自己,而是要进行深度复制,
* 把整个数组复制一遍,才能够防止数据污染
*/
std::copy(Another.Data, Another.Data + 10000000, this->Data);
}
}

void Func(Test t)
{
// ...
}

void main()
{
Person p;
func(p); // Func的参数是值传递,会隐式调用`Test(const Test& Another)`来构建参数
}

上面这个例子其实也没啥问题,传进去一个左值p,这个左值我们可能在main函数的定义域内还要做其他修改,当然不能够简单地浅复制解决。

但是假如我们是传进一个临时变量,那么再把这个大数组进行完整地复制,那就是多此一举了。最好的就是浅复制,直接拿到这个int数组。

1
2
3
4
void main()
{
func(Person());
}

这个时候我们要多提供一个右值引用的构造函数:

1
2
3
4
5
6
7
8
9
/**
* 进来这个版本的构造函数了,说明传进来的是右值,可以毫不留情地掠夺资源
*/
Test(Test&& Another)
{
this->Data = Another.Data;
/** 把右值对象的内容置空,避免源对象析构时影响本对象 */
Another.Data = nullptr;
}

如果我们能够保证左值在传入右值引用函数之后,不再使用,那么我们也可以把左值赋予给右值引用(抛弃自己的所有权,但是只能我们自己保证,编译器不会禁止继续使用和更改该左值)。

1
2
3
4
5
void main()
{
Person p;
func(move(p));
}

禁止默认的const Class&构造函数

1
Test(const Test& Another) = delete;

如果我们delete了默认的拷贝构造函数,只实现移动构造函数,那么在使用拷贝构造函数的时候(不管隐式还是显示)就会编译报错,相当于强迫使用者使用右值引用。

1
2
3
4
Person a;
Func(a); // error 左值不能转换成右值引用
Func(std::move(a)); // ok a被转换成右值,但是最好保证后面不会在a的定义域再使用到a
Func(Person()); // ok

最后再举个例子

1
2
3
4
5
6
7
8
9
10
void Func()
{
Test ret;
return ret;
}

int main()
{
Test a = Func();
}

上面这个过程总共发生了三次构造:

  1. Func()中使用默认构造函数构建了左值ret
  2. Func()返回了一个值,众所周知函数的传入与传出都是要进行一次值复制的,所以进行一次复制
  3. Test a使用Func()返回的结果作为参数传给构造函数

假设Test类中含有很大的一个数组,光是使用Test(const Test& Another)这种拷贝构造函数,我们没有办法区分清楚传进来的是左值还是右值,所以只能一律当做左值来处理。

如果是左值,那意味着传进来的这个引用,外界还会用到,并且有可能会对其内容做修改。所以我们需要进行深复制,把整个大数组拷贝一份。而如果传进来的是右值,那么亏了,明明我们知道这个右值被传进来使用过一次之后就会被外界销毁(右值都是一些会被销毁的临时变量),但是因为我们不知道究竟是不是右值,所以还是要把大数组整个拷贝一次。

而如果我们重写了一个Test(Test&& Another),当传进来的是一个右值的时候,会优先调用此构造函数。此时我们明确知道传进来的是一个右值,阅后即焚的那种,所以我们不必担心我们的修改会影响到这个变量。因此,我们直接把这个右值的内容拿过来用就行了。也就是说,直接把数组的地址拷贝过来,而不需要拷贝一整个数组。

拷贝构造函数&赋值运算符的区别

本质区别:拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例

换言之,一个会生成新的实例,而一个是对已存在实例进行数据更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Func(Person p){}

void Func2()
{
Person p;
return p;
}

int main()
{
Person p;
Person p1 = p; // 构造
Person p2;
p2 = p; // 复制
Func(p); // 构造
p2 = Func2(); // 先构造(`Func2`返回一个值对象),然后复制给p2

Person p3 = Func2(); // 本来是应该用构造创建一个函数返回值,然后再调用构造生成p3.
//但是编译器优化过,导致直接用返回的右值赋予了给p3。
}
Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//