20240617 C++ 右值 的验证困惑/省略复制
明昧 Lv7

C++11

右值引用

这玩意是什么来着

反正是有左值和右值的区别来着

左值好像是一个有实际内存的书

右值是一个没有实际内存的数

(我可以这样理解吗?)


那么左值引用和右值引用的区别是什么呢?

为什么C++11中需要纳入右值引用?是为了解决什么问题呢?

或者是说能够实现什么优化呢?

为了减小开销,实际上move函数本身不是真正意义上实现“拷贝复制”,而是由对象b接受右值对象a的所有资源(如果底层是由指针来控制的话),并且将对象a 的控制权置空

右值引用理解和测试

image-20240617204552086

报错原因:编码问题

右值机制验证

  • 预设或者问题:在转移a资源的掌控之后,a会在什么时候被调用析构函数?以及被析构之前的指针指向究竟是什么?对象们的被析构顺序是?

  • 验证代码

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<iostream>

class MyClass {
public:
MyClass() : data(new int[1000])
{
std::cout << "MyClass has born!!" << this<<" " << data << std::endl;
}

~MyClass()
{
std::cout << "MyClass is dying!!" << this <<" " << data << std::endl;
delete[] data; // 释放资源
}

// 拷贝构造函数
MyClass(const MyClass& other)
{
std::cout << "copy ing!" << this << " " << data << std::endl;
data = new int[1000];
std::copy(other.data, other.data + 1000, data);
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data)
{
std::cout << "right value to!" << this <<" " << data << std::endl;
other.data = nullptr; // 将源对象的指针设为 nullptr
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept
{
std::cout << "equal to ing" << this << " " << data << std::endl;
if (this != &other) {
delete[] data; // 释放当前对象的资源
data = other.data; // 转移资源
other.data = nullptr; // 将源对象的指针设为 nullptr
}
return *this;
}

private:
int* data;
};

int main() {
MyClass a;
MyClass b(std::move(a)); // 移动构造函数被调用,a 的资源被转移到 b

MyClass c(b);
// 此时 a.data 是 nullptr,不会释放任何资源
return 0; // b 的析构函数释放资源,a 的析构函数不释放任何资源
}

image-20240617210044731

不对不对

我们是在验证右值

那么

这个已经被定义不算右值吧

但是可以被肯定的一点是

move()函数本身是需要用到右值移动函数的

1
2
3
4
5
6
7
MyClass d;
MyClass a;
MyClass b(std::move(a)); // 移动构造函数被调用,a 的资源被转移到 b

MyClass c(b);
// 此时 a.data 是 nullptr,不会释放任何资源
return 0; // b 的析构函数释放资源,a 的析构函数不释放任何资源

image-20240617210804193

很明显看出被析构顺序的规律:谁先被创建谁就最后被析构

image-20240617212226782

??这里应该是右值,为什么为什么不行

这里应该是调用了移动赋值函数,怎么不行呢?

image-20240617212455015

加了一个还是不行

不知道为什么

它无论是左值的调用还是右值的调用都相当奇怪

是我们自己的加法有问题好吧

修改成int来试一下

我们要解决的问题是看清楚右值被调用的时机

image-20240617214849304

为什么

为什么

MyClass c这一行右值没有被隐式调用右值引用

靠靠靠靠靠靠靠靠靠

image-20240618123245299

尝试在stackflow中问答

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

class MyClass {
public:
MyClass() : data(new int[1]{1})
{
std::cout << "MyClass has born!!" << this<<" " << data << std::endl;
}

~MyClass()
{
std::cout << "MyClass is dying!!" << this <<" " << data << std::endl;
delete[] data;
}


MyClass(int k)
{

data = new int[1];
data[0] = k;
std::cout << "copy ing kkkkkkk" << this << " " << data << std::endl;

}



MyClass(const MyClass& other)
{
std::cout << "copy ing!" << this << " " << data << std::endl;
data = new int[1];
std::copy(other.data, other.data + 1, data);
}


MyClass(MyClass&& other) noexcept : data(other.data)
{
std::cout << "right value to!" << this <<" " << data << std::endl;
other.data = nullptr;
}


MyClass& operator=(MyClass&& other) noexcept
{
std::cout << "equal to r ing" << this << " " << data << std::endl;
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}


MyClass& operator=(MyClass& other) noexcept
{
std::cout << "equal to l ing" << this << " " << data << std::endl;
if (this != &other) {
delete[] data;
data = new int[1];
*data = *other.data;
}
return *this;
}


friend MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
return MyClass(*lhs.data + *rhs.data);
}

private:
int* data;
};

int main() {
MyClass d;
MyClass a;
MyClass b(std::move(a));

MyClass c = b + d;


return 0;
}

答复:复制省略

https://en.cppreference.com/w/cpp/language/copy_elision

好像我看下来是编译器进行了复制省略

我还是不太懂

复制省略

定义

C++ 的省略复制(copy elision)是一种编译器优化技术,用于减少不必要的对象拷贝和临时对象的创建,从而提高程序的效率。在某些情况下,C++ 标准允许编译器跳过对象的复制和移动操作,即使这些操作在代码中显式地存在。这种优化在某些场景下是强制性的,而在另一些场景下则是可选的。

实现时机

参考网址

https://en.cppreference.com/w/cpp/language/copy_elision

https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization

chatgpt

https://www.geeksforgeeks.org/copy-elision-in-cpp/

https://www.youtube.com/watch?v=HNYOx-Vh_VA

自 C++17 以来,以下几种情况下编译器必须执行省略复制:

1.返回值优化(Return Value Optimization, RVO)

2.NRVO(Named Return Value Optimization)

1.RVO

旧情景:先在被调用函数的栈空间中创建该对象再复制或移动到调用者的空间。

优化成 ------->

新情景:当函数返回一个局部对象时,编译器可以直接在调用者的存储空间中构造这个对象


2.NRVO

当函数返回一个命名的局部对象时,编译器可以直接在调用者的存储空间中构造这个对象。

1
2
3
4
MyClass createObject() {
MyClass obj;
return obj; // 这里省略了构造临时对象和移动操作
}

实现机制

1. 返回值优化(RVO)

RVO 是最常见的一种省略复制优化技术。它在函数返回一个局部对象时,直接在调用者的内存空间中构造该对象,避免了临时对象的创建和后续的复制或移动操作。

实现原理:

  • 编译器检测到函数返回一个局部对象时,它会修改返回对象的存储位置,使其直接指向调用者的内存空间。
  • 编译器在生成代码时,通过调整栈帧和对象的构造位置,确保对象直接在目标位置构造,而不在函数的栈空间中临时创建。

2. 命名返回值优化(NRVO)

NRVO 是 RVO 的扩展,适用于函数返回命名的局部对象的情况。

实现原理:

  • 编译器跟踪命名对象的生命周期和返回路径,当确定返回的对象可以安全地在调用者的内存空间中构造时,它会进行相应的优化。
  • 与 RVO 类似,编译器在代码生成阶段调整对象的构造位置和栈帧,使对象直接在目标位置构造。

3. 临时对象的优化

除了函数返回值优化,编译器还可以在其他场景中优化临时对象的创建,例如在表达式中创建的临时对象

实现原理:

  • 编译器在表达式分析阶段,检测临时对象的创建和使用情况,尽量避免不必要的对象复制。
  • 通过内联展开和临时对象的合并,编译器可以减少临时对象的创建和析构次数。

4. 特定编译器的实现细节

不同的编译器可能有不同的实现细节,但总体思路是一致的。以下是一些具体编译器的实现方法:

GCC(GNU Compiler Collection)

GCC 通过在代码生成阶段对返回对象的存储位置进行优化来实现 RVO 和 NRVO。它使用内部数据结构和优化算法,确保对象在最合适的位置构造。

Clang/LLVM

Clang 使用 LLVM 中间表示(IR)进行优化。在生成 LLVM IR 时,Clang 会标记返回对象的存储位置,并在优化过程中合并临时对象,减少不必要的复制和移动操作。

MSVC(Microsoft Visual C++)

MSVC 通过在中间代码生成阶段优化对象的构造位置,利用其内部的优化算法和数据流分析技术,确保对象在最有效的位置构造,避免不必要的临时对象创建。

5. 标准库支持

C++ 标准库也支持省略复制。例如,在容器操作中(如 std::vectoremplace_back),标准库函数会直接在目标位置构造对象,避免额外的临时对象创建。

结论

省略复制的实现依赖于编译器的智能优化技术,通过调整对象的构造位置和生命周期管理,避免不必要的复制和移动操作。这不仅提高了程序的性能,还减少了内存的使用和管理负担。现代编译器在这方面都有很好的支持,使得开发者可以在编写高效代码的同时,不需要过多关注底层的优化细节。

视频中的代码分析

image-20240618191753558

每一个过程都要看清楚wow

每一个复制过程中都是在怎样的内存机制和数据行为下发生的!看清楚

https://www.youtube.com/watch?v=HNYOx-Vh_VA

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View