提问人:bigcodeszzer 提问时间:2/9/2016 最后编辑:bigcodeszzer 更新时间:2/10/2016 访问量:1164
移动赋值运算符、移动构造函数
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'
不需要解释 rvalues、lvalues 或模板,我甚至可以说这些主题是无关的。
第一个例子比第二个例子快,这一事实应该是给定的。我还要指出,在 C++ 11 之前,任何有效的代码都会这样做。
据我了解,这个想法是将所有这些行为捆绑在 std 库中一个简洁的小运算符 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);
}
我很确定这是行不通的,所以我的问题是:你如何通过移动语义实现这种类型的构造函数功能?
答:
我并不完全清楚你的代码示例应该证明什么——或者这个问题的重点是什么。
从概念上讲,短语“移动语义”在C++中是什么意思吗?
是“如何编写移动 ctor 和移动赋值运算符? ?
这是我试图介绍这个概念的尝试。如果要查看代码示例,请查看注释中链接的任何其他 SO 问题。
直观地说,在 C 和 C++ 中,一个对象应该表示驻留在内存中的一段数据。出于多种原因,您通常希望将该数据发送到其他位置。
通常,人们可以采取直接的方法,简单地将对对象的指针/引用传递到需要数据的位置。然后,可以使用指针读取它。拿起指针并四处移动指针非常便宜,因此这通常非常有效。主要缺点是你必须确保对象将存在所需的时间,否则你会得到一个悬空的指针/引用和崩溃。有时这很容易确保,有时则不然。
如果不是,一个明显的替代方法是制作副本并传递它(按值传递),而不是按引用传递。当需要数据的地方有自己的个人数据副本时,它可以确保副本在需要时保留。这里的主要缺点是你必须制作一个副本,如果对象很大,这可能会很昂贵。
第三种选择是移动对象而不是复制它。移动对象时,它不会被复制,而是在新网站中以独占方式提供,而不再在旧网站中可用。显然,只有当您在旧站点不再需要它时,您才能这样做,但在这种情况下,这会为您节省一份副本,从而节省大量资金。
当对象很简单时,所有这些概念对于实际实现和正确来说都是相当微不足道的。例如,当你有一个对象时,即一个具有微不足道的构造/破坏的对象,可以安全地像在 C 编程语言中所做的那样,使用 . 生成字节块的逐字节副本。如果一个普通的对象被正确初始化,因为它的创建没有可能的副作用,而它后来的销毁也没有,那么复制也会被正确初始化并产生一个有效的对象。trivial
memcpy
memcpy
memcpy
但是,在现代 C++ 中,许多对象都不是微不足道的——它们可能“拥有”对堆内存的引用,并使用 RAII 管理此内存,RAII 将对象的生存期与某些资源的使用联系起来。例如,如果你在一个函数中有一个局部变量,那么字符串并不完全是一个“连续”的对象,而是连接到内存中的两个不同位置。堆栈上有一个固定大小(实际上是)的小块,其中包含一个指针和一些其他信息,指向堆上动态大小的缓冲区。从形式上讲,只有小的“控制”部分是对象,但从程序员的角度来看,缓冲区也是字符串的“部分”,是你通常想到的部分。你不能使用这样的对象来复制 -- 想想如果你有,你尝试从地址复制字节来获得第二个字符串会发生什么。最终将得到两个控制块,而不是两个不同的字符串对象,每个控制块都指向同一个缓冲区。当第一个缓冲区被销毁时,该缓冲区将被删除,因此使用第二个缓冲区将导致段错误,或者当第二个缓冲区被销毁时,您将获得双重删除。std::string
sizeof(std::string)
std::string
std::string
memcpy
std::string s
sizeof(std::string)
&s
通常,复制非平凡的 C++ 对象是非法的,并会导致未定义的行为。这是因为它与 C++ 的核心思想之一相冲突,即对象的创建和销毁可能会产生由程序员使用 ctor 和 dtors 定义的非平凡后果。对象生存期可用于创建和强制执行不变量,您可以使用这些变量来推理程序。 是一种“愚蠢”的低级方法,只复制一些字节 - 它可能绕过强制执行使程序工作的不变量的机制,这就是为什么如果使用不当,它会导致未定义的行为。memcpy
memcpy
相反,在 C++ 中,我们有复制构造函数,您可以使用它们安全地复制非平凡对象。您应该以保留对象所需的不变量的方式编写这些内容。三法则是关于如何实际做到这一点的指导方针。
C++11 的“移动语义”思想是新的核心语言特性的集合,这些特性是为了扩展和完善 C++98 中的传统复制构造机制而添加的。具体来说,它是关于我们如何移动潜在复杂的 RAII 对象,而不仅仅是我们已经能够移动的琐碎对象。我们如何使语言在可能的情况下自动为我们生成移动构造函数等,类似于它为复制构造函数所做的那样。我们如何让它尽可能使用移动选项来节省我们的时间,而不会在旧代码中引起错误,或破坏语言的核心假设。(这就是为什么我会说你的代码示例与 's 和 's 与 C++11 移动语义关系不大。int
int *
因此,五法则是三法则的相应扩展,它描述了您可能还需要为给定类实现移动 ctor / 移动赋值运算符并且不依赖于语言的默认行为的条件。
评论
上一个:什么是 C++ 中的资源?
下一个:什么是三分法则?
评论
data = movefrom.data