移动赋值运算符,移动构造函数
Move assignment operator, move constructor
我一直在努力确定 5 的规则,但网上的大部分信息都过于复杂,示例代码也不尽相同。
即使我的教科书也没有很好地涵盖这个主题。
关于移动语义:
撇开模板、右值和左值不谈,据我了解,移动语义很简单:
int other = 0; //Initial value
int number = 3; //Some data
int *pointer1 = &number; //Source pointer
int *pointer2 = &other; //Destination pointer
*pointer2 = *pointer1; //Both pointers now point to same data
pointer1 = nullptr; //Pointer2 now points to nothing
//The reference to 'data' has been 'moved' from pointer1 to pointer2
相对于复制,这相当于这样的事情:
pointer1 = &number; //Reset pointer1
int newnumber = 0; //New address for the data
newnumber = *pointer1; //Address is assigned value
pointer2 = &newnumber; //Assign pointer to new address
//The data from pointer1 has been 'copied' to pointer2, at the address 'newnumber'
不需要解释右值、左值或模板,我什至可以说这些主题是不相关的。
第一个示例比第二个示例更快这一事实应该是给定的。我还要指出,C++ 11 之前的任何高效代码都可以做到这一点。
根据我的理解,这个想法是将所有这些行为捆绑在标准库中一个简洁的小运算符 move() 中。
在编写拷贝构造函数和拷贝赋值运算符时,我简单地这样做:
Text::Text(const Text& copyfrom) {
data = nullptr; //The object is empty
*this = copyfrom;
}
const Text& Text::operator=(const Text& copyfrom) {
if (this != ©from) {
filename = copyfrom.filename;
entries = copyfrom.entries;
if (copyfrom.data != nullptr) { //If the object is not empty
delete[] data;
}
data = new std::string[entries];
for (int i = 0; i < entries; i++) {
data[i] = copyfrom.data[i];
//std::cout << data[i];
}
std::cout << "Data is assigned" << std::endl;
}
return *this;
}
人们会认为,等效的是:
Text::Text(Text&& movefrom){
*this = movefrom;
}
Text&& Text::operator=(Text&& movefrom) {
if (&movefrom != this) {
filename = movefrom.filename;
entries = movefrom.entries;
data = movefrom.data;
if (data != nullptr) {
delete[] data;
}
movefrom.data = nullptr;
movefrom.entries = 0;
}
return std::move(*this);
}
我很确定这行不通,所以我的问题是:如何使用移动语义实现这种类型的构造函数功能?
我不太清楚你的代码示例应该证明什么——或者这个问题的重点是什么。
概念上短语'move semantics'在C++中是什么意思?
是"how do I write move ctors and move assignment operators?"吗?
这是我尝试介绍的概念。如果您想查看代码示例,请查看评论中链接的任何其他 SO 问题。
直觉上,在 C 和 C++ 中,一个对象应该表示驻留在内存中的一段数据。出于多种原因,通常您想将该数据发送到其他地方。
通常可以采取一种直接的方法,将对象的指针/引用简单地传递到需要数据的地方。然后,可以使用指针读取它。获取指针并四处移动指针非常便宜,因此这通常非常有效。主要缺点是您必须确保该对象会在需要时一直存在,否则您会得到悬空指针/引用和崩溃。有时这很容易确保,有时则不然。
如果不是,一个明显的替代方法是复制并传递它(按值传递)而不是按引用传递。当需要数据的地方有自己的数据副本时,它可以确保副本在需要时一直存在。这里的主要缺点是你必须制作一个副本,如果对象很大,这可能会很昂贵。
第三种选择是移动对象而不是复制它。当一个对象被移动时,它不会被复制,而是在新站点中独占可用,而不再在旧站点中可用。显然,您只能在旧站点不再需要它时执行此操作,但在那种情况下,这会为您节省一份副本,这可以节省一大笔钱。
当对象很简单时,所有这些概念在实际实施和正确实施时都相当微不足道。例如,当你有一个 trivial
对象时,也就是说,一个具有简单构造/破坏的对象,可以安全地像在 C 编程语言中那样使用 memcpy
完全复制它。 memcpy
生成字节块的逐字节副本。如果一个普通对象被正确初始化,因为它的创建没有可能的副作用,而且它后来的销毁也没有,那么 memcpy
副本也被正确初始化并产生一个有效的对象。
然而,在现代 C++ 中,您的许多对象都不是微不足道的——它们可能 "own" 引用堆内存,并使用 RAII 管理此内存,RAII 将对象的生命周期与某些对象的使用联系起来资源。例如,如果您有一个 std::string
作为函数中的局部变量,则该字符串不完全是一个 "contiguous" 对象,而是连接到内存中的两个不同位置。堆栈上有一个固定大小(sizeof(std::string)
,事实上)的小块,其中包含一个指针和一些其他信息,指向堆上一个动态大小的缓冲区。形式上,只有小的 "control" 部分是 std::string
对象,但从程序员的角度来看,缓冲区也是字符串的 "part" 并且是您通常认为的部分。你不能像这样使用 memcpy
复制一个 std::string
对象——想想如果你有 std::string s
并且你尝试从地址 [=] 复制 sizeof(std::string)
字节会发生什么21=] 得到第二个字符串。您将得到两个控制块,而不是两个不同的字符串对象,每个控制块都指向同一个缓冲区。当第一个被销毁时,那个缓冲区被删除,所以使用第二个会导致段错误,或者当第二个被销毁时,你会得到双重删除。
通常,使用 memcpy
复制重要的 C++ 对象是非法的,并且会导致未定义的行为。这是因为它与 C++ 的核心思想之一相冲突,即对象的创建和销毁可能会产生由程序员使用 ctors 和 dtor 定义的重要后果。对象生命周期可用于创建和强制执行用于推理程序的不变量。 memcpy
是一种仅复制一些字节的 "dumb" 低级方法——它可能会绕过强制执行使程序运行的不变量的机制,这就是如果使用不当会导致未定义行为的原因。
相反,在 C++ 中,我们有复制构造函数,您可以使用它来安全地制作重要对象的副本。您应该以保留对象所需的不变量的方式编写这些。三法则是关于如何实际做到这一点的指南。
C++11 "move semantics" 思想是新的核心语言特性的集合,添加这些特性是为了扩展和改进 C++98 中的传统复制构造机制。具体来说,它是关于我们如何移动潜在的复杂 RAII 对象,而不仅仅是我们已经能够移动的微不足道的对象。我们如何使语言在可能的情况下自动为我们生成移动构造函数等,类似于它为复制构造函数所做的那样。我们如何让它在可以节省时间的情况下使用移动选项,而不会在旧代码中引起错误,或者破坏语言的核心假设。 (这就是为什么我会说你的 int
和 int *
的代码示例与 C++11 移动语义没有什么关系。)
然后,五规则是三规则的相应扩展,它描述了您可能还需要为给定 class 实施移动构造函数/移动赋值运算符而不依赖于语言的默认行为。
我一直在努力确定 5 的规则,但网上的大部分信息都过于复杂,示例代码也不尽相同。
即使我的教科书也没有很好地涵盖这个主题。
关于移动语义:
撇开模板、右值和左值不谈,据我了解,移动语义很简单:
int other = 0; //Initial value
int number = 3; //Some data
int *pointer1 = &number; //Source pointer
int *pointer2 = &other; //Destination pointer
*pointer2 = *pointer1; //Both pointers now point to same data
pointer1 = nullptr; //Pointer2 now points to nothing
//The reference to 'data' has been 'moved' from pointer1 to pointer2
相对于复制,这相当于这样的事情:
pointer1 = &number; //Reset pointer1
int newnumber = 0; //New address for the data
newnumber = *pointer1; //Address is assigned value
pointer2 = &newnumber; //Assign pointer to new address
//The data from pointer1 has been 'copied' to pointer2, at the address 'newnumber'
不需要解释右值、左值或模板,我什至可以说这些主题是不相关的。
第一个示例比第二个示例更快这一事实应该是给定的。我还要指出,C++ 11 之前的任何高效代码都可以做到这一点。
根据我的理解,这个想法是将所有这些行为捆绑在标准库中一个简洁的小运算符 move() 中。
在编写拷贝构造函数和拷贝赋值运算符时,我简单地这样做:
Text::Text(const Text& copyfrom) {
data = nullptr; //The object is empty
*this = copyfrom;
}
const Text& Text::operator=(const Text& copyfrom) {
if (this != ©from) {
filename = copyfrom.filename;
entries = copyfrom.entries;
if (copyfrom.data != nullptr) { //If the object is not empty
delete[] data;
}
data = new std::string[entries];
for (int i = 0; i < entries; i++) {
data[i] = copyfrom.data[i];
//std::cout << data[i];
}
std::cout << "Data is assigned" << std::endl;
}
return *this;
}
人们会认为,等效的是:
Text::Text(Text&& movefrom){
*this = movefrom;
}
Text&& Text::operator=(Text&& movefrom) {
if (&movefrom != this) {
filename = movefrom.filename;
entries = movefrom.entries;
data = movefrom.data;
if (data != nullptr) {
delete[] data;
}
movefrom.data = nullptr;
movefrom.entries = 0;
}
return std::move(*this);
}
我很确定这行不通,所以我的问题是:如何使用移动语义实现这种类型的构造函数功能?
我不太清楚你的代码示例应该证明什么——或者这个问题的重点是什么。
概念上短语'move semantics'在C++中是什么意思?
是"how do I write move ctors and move assignment operators?"吗?
这是我尝试介绍的概念。如果您想查看代码示例,请查看评论中链接的任何其他 SO 问题。
直觉上,在 C 和 C++ 中,一个对象应该表示驻留在内存中的一段数据。出于多种原因,通常您想将该数据发送到其他地方。
通常可以采取一种直接的方法,将对象的指针/引用简单地传递到需要数据的地方。然后,可以使用指针读取它。获取指针并四处移动指针非常便宜,因此这通常非常有效。主要缺点是您必须确保该对象会在需要时一直存在,否则您会得到悬空指针/引用和崩溃。有时这很容易确保,有时则不然。
如果不是,一个明显的替代方法是复制并传递它(按值传递)而不是按引用传递。当需要数据的地方有自己的数据副本时,它可以确保副本在需要时一直存在。这里的主要缺点是你必须制作一个副本,如果对象很大,这可能会很昂贵。
第三种选择是移动对象而不是复制它。当一个对象被移动时,它不会被复制,而是在新站点中独占可用,而不再在旧站点中可用。显然,您只能在旧站点不再需要它时执行此操作,但在那种情况下,这会为您节省一份副本,这可以节省一大笔钱。
当对象很简单时,所有这些概念在实际实施和正确实施时都相当微不足道。例如,当你有一个 trivial
对象时,也就是说,一个具有简单构造/破坏的对象,可以安全地像在 C 编程语言中那样使用 memcpy
完全复制它。 memcpy
生成字节块的逐字节副本。如果一个普通对象被正确初始化,因为它的创建没有可能的副作用,而且它后来的销毁也没有,那么 memcpy
副本也被正确初始化并产生一个有效的对象。
然而,在现代 C++ 中,您的许多对象都不是微不足道的——它们可能 "own" 引用堆内存,并使用 RAII 管理此内存,RAII 将对象的生命周期与某些对象的使用联系起来资源。例如,如果您有一个 std::string
作为函数中的局部变量,则该字符串不完全是一个 "contiguous" 对象,而是连接到内存中的两个不同位置。堆栈上有一个固定大小(sizeof(std::string)
,事实上)的小块,其中包含一个指针和一些其他信息,指向堆上一个动态大小的缓冲区。形式上,只有小的 "control" 部分是 std::string
对象,但从程序员的角度来看,缓冲区也是字符串的 "part" 并且是您通常认为的部分。你不能像这样使用 memcpy
复制一个 std::string
对象——想想如果你有 std::string s
并且你尝试从地址 [=] 复制 sizeof(std::string)
字节会发生什么21=] 得到第二个字符串。您将得到两个控制块,而不是两个不同的字符串对象,每个控制块都指向同一个缓冲区。当第一个被销毁时,那个缓冲区被删除,所以使用第二个会导致段错误,或者当第二个被销毁时,你会得到双重删除。
通常,使用 memcpy
复制重要的 C++ 对象是非法的,并且会导致未定义的行为。这是因为它与 C++ 的核心思想之一相冲突,即对象的创建和销毁可能会产生由程序员使用 ctors 和 dtor 定义的重要后果。对象生命周期可用于创建和强制执行用于推理程序的不变量。 memcpy
是一种仅复制一些字节的 "dumb" 低级方法——它可能会绕过强制执行使程序运行的不变量的机制,这就是如果使用不当会导致未定义行为的原因。
相反,在 C++ 中,我们有复制构造函数,您可以使用它来安全地制作重要对象的副本。您应该以保留对象所需的不变量的方式编写这些。三法则是关于如何实际做到这一点的指南。
C++11 "move semantics" 思想是新的核心语言特性的集合,添加这些特性是为了扩展和改进 C++98 中的传统复制构造机制。具体来说,它是关于我们如何移动潜在的复杂 RAII 对象,而不仅仅是我们已经能够移动的微不足道的对象。我们如何使语言在可能的情况下自动为我们生成移动构造函数等,类似于它为复制构造函数所做的那样。我们如何让它在可以节省时间的情况下使用移动选项,而不会在旧代码中引起错误,或者破坏语言的核心假设。 (这就是为什么我会说你的 int
和 int *
的代码示例与 C++11 移动语义没有什么关系。)
然后,五规则是三规则的相应扩展,它描述了您可能还需要为给定 class 实施移动构造函数/移动赋值运算符而不依赖于语言的默认行为。