C++学习笔记----8、掌握类与对象(一)---- 对象中的动态内存分配(6)
2.4.5、移动对象数据成员
moveFrom()成员函数使用三个数据成员的直接赋值,因为它们是原始数据类型。如果对象包含其他对象作为数据成员,应该使用std::move()移动这些对象。假定Spreadsheet类有一个std::string数据成员叫做m_name。moveFrom()成员函数应该实现如下:
void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
// Move object data members
m_name = std::move(src.m_name);
// Move primitives:
m_width = exchange(src.m_width, 0);
m_height = exchange(src.m_height, 0);
m_cells = exchange(src.m_cells, nullptr);
}
2.4.6、用swap来实现move构造函数与move赋值操作符
前面对move构造函数与move赋值操作符的实现都使用了moveFrom()辅助函数,通过执行shallow拷贝移动所有的数据成员。在这种实现方式下,如果想要给Spreadsheet类添加一个新的数据成员,就需要修改swap()函数与moveFrom()函数。如果忘记了更新其中的一个,就会引起bug。为了避免这种bug,可以使用swap()函数来写move构造函数与move赋值操作符。
首先,可以不用cleanup()与moveFrom()辅助函数了。cleanup()函数的代码被移到了析构函数中。move构造函数与move赋值操作符可以实现如下:
// Move constructor
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
{
println("Move constructor");
swap(src);
}
// Move assignment operator
Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
println("Move assignment operator");
auto moved{ std::move(rhs) }; // Move rhs into moved (noexcept)
swap(moved); // Commit the work with only non-throwing operations
return *this;
}
move构造函数简单地用给定的源对象来交换缺省构造的*this。move赋值操作符使用move-and-swap习语,与我们以前讨论的copy-and-swap习语类似。
注意:使用swap()实现move构造函数与move赋值操作符代码更少。在添加数据成员时更不容易引起bug,因为只需要更新swap()实现,在其中包含新的数据成员就行。
Spreadsheet的move赋值操作符也可以实现如下:
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs) noexcept
{
swap(rhs);
return *this;
}
然而,这样做不能保证this的内容被马上清理。实际上,this的内容通过rhs离开move赋值操作符的调用,因此可能比预期的存活时间要长。
2.4.7、测试Spreadsheet move操作
可以用下面的代码来测试move构造函数与move赋值操作符:
import std;
import spreadsheet;
using namespace std;
Spreadsheet createObject()
{
return Spreadsheet { 3, 2 };
}
int main()
{
vector<Spreadsheet> vec;
for (size_t i{ 0 }; i < 2; ++i) {
println("Iteration {}", i);
vec.push_back(Spreadsheet { 100, 100 });
println("");
}
Spreadsheet s { 2, 3 };
s = createObject();
println("");
Spreadsheet s2 { 5, 6 };
s2 = s;
}
前面多次提到过vector。vector可以动态增长以容纳新的对象。是通过分配更大的内存,然后拷贝或者从旧的vector移动到新的更大的vecotr来实现的。如果编译器发现noexcept move构造函数,对象就使用move而不是拷贝。因为它们是被移动的,所以没有必要进行深层次的拷贝,可以使效率更高。
在对Spreadsheet的所有的构造函数与赋值操作符加上打印语句后,前面的测试程序的输出可能如下。每行的右边的数字不是输出的一部分,加上它纯粹是为了继续讨论的方便,因为它指的是确定的行数。该输出以及下面的讨论都是基于使用move-and-swqp习语来实现其move操作的Spreadsheet类的版本,在Microsoft Visual C++ 2022编译环境生成的代码构建。c++标准没有指定vector的初始大小,也没有给出增长策略,所以不同的编译器其输出可能是不同的。
Iteration 0
Normal constructor (1)
Move constructor (2)
Iteration 1
Normal constructor (3)
Move constructor (4)
Move constructor (5)
Normal constructor (6)
Normal constructor (7)
Move assignment operator (8)
Move constructor (9)
Normal constructor (10)
Copy assignment operator (11)
Normal constructor (12)
Copy constructor (13)
在循环的第一次迭代中,vector仍然是空的,使用下面循环中的代码语句:
vec.push_back(Spreadsheet { 100, 100 });
这行代码执行后,新的Spreadsheet对象生成,触发了正常的构造函数(1)。vector改变其大小来扩大空间以容纳压入的新的对象。生成的Spreadsheet对象被移入vector,触发move构造函数(2)。
在循环的第二次迭代中,第二个Spreadsheet对象使用正常的构造函数(3)生成。在这一点上,vector可以容纳一个元素,所以它再一次调整大小来扩大空间以容纳第二个对象。因为vector调整了空间,前面添加的元素需要从旧的vector移到新的更大的vector中。这触发了一个对于每一个前面添加的元素的move构造函数的调用。在vector中有一个元素,所以move构造函数被调用了一次(4)。最后,新的Spreadsheet对象使用move构造函数(5)被移动到vector中。
接下来,Spreadsheet对象s使用正常的构造函数(6)生成。createObject()函数使用正常的构造函数(7)生成一个临时的Spreadsheet对象,构造函数返回值赋给变量s。因为从createObject()返回的临时对象在赋值之后就会消失,编译器触发了move赋值操作符(8)而不是拷贝赋值操作符。move赋值操作符使用move-and-swap习语,所以它代理了move构造函数(9)的工作。
另一个Spreadsheet对象生成,s2,使用正常的构造函数(10)。赋值运算s2 = s触发了拷贝赋值操作符(11),因为右手边的对象不是一个临时对象,它是一个命名对象。该拷贝赋值操作符使用copy-and-swap习语,生成一个临时拷贝,触发对于拷贝构造函数的调用,它首先代理正常的构造函数(12和13)。
如果Spreadsheet类没有实现move的语法,所有对move构造函数与move赋值操作符的调用都会被替换成对拷贝构造函数与拷贝赋值操作符的调用。在前面的例子中,Spreadsheet对象在循环中要调用10000(100*100)个元素。Spreadsheet move构造函数与move赋值操作符的实现不需要任何的内存分配,而拷贝构造函数与拷贝赋值操作符每次需要101个内存分配。所以,在特定情况下使用move语法可以大大提高性能。