我观察到一种奇怪的行为,我不太了解析构函数和(默认)复制和移动赋值。
假设我有一个具有默认所有内容的类B
和一个具有自定义析构函数、默认复制赋值和(可能)默认移动赋值的类Test
。
然后我们创建一个B
的实例,将其分配给一个变量,并使用赋值替换为一个新实例(其中右侧是右值)。
有两件事对我来说似乎很奇怪,我在文档中看不到它们的原因。
- 当
Test
没有move assignment
(因此调用其复制赋值)时,不会显式调用T1
对象的析构函数。我假设在这种情况下,惯用的做法是将资源作为copy assignment
的一部分进行清理。然而,为什么当move assignment
在那里(并被召唤)时会有所不同?如果在那里,Test
的析构函数被显式调用(由运算符调用)。 - 文档指定移动分配后的
other
可以保留为任何状态。为什么在 B 的成员没有move assignment
的情况下不调用 T2 的时间右值(即=B("T2")
的右侧)的析构函数?
游乐场代码:https://onlinegdb.com/S1lCYmkKOV
#include <iostream>
#include <string>
class Test
{
public:
std::string _name;
Test(std::string name) : _name(name) { }
~Test()
{
std::cout << "Destructor " << _name << std::endl;
}
Test& operator=(const Test& fellow) = default;
//Test & operator= ( Test && ) = default;
};
class B {
public:
Test t;
B() : t("T0") {}
B(std::string n) : t(n) {}
};
int fce(B& b)
{
std::cout << "b = B(T2)n";
b = B("T2");
std::cout << "return 0n";
return 0;
}
int main() {
B b("T1");
std::cout << "fce calln";
fce(b);
std::cout << "fce end " << b.t._name << std::endl;
}
带移动的输出:
fce call
b = B(T2)
Destructor T1
return 0
fce end T2
Destructor T2
不移动输出:
fce call
b = B(T2)
Destructor T2
return 0
fce end T2
Destructor T2
默认移动分配调用析构函数,复制分配不
这两个赋值都会导致临时B
对象的销毁,因此调用析构函数。
使用赋值替换为新实例
迂腐的注释:赋值不会替换实例。实例保持不变;修改实例的值。这种区别可能是微妙的,但也可能与你的困惑有关。
当 Test 没有移动赋值(因此调用其复制赋值)时,不会显式调用 T1 对象的析构函数。
目前还不清楚您所说的"T1 对象"是什么意思。使用"T1"
初始化的变量b
将被销毁。但是当它被销毁时,它的值之前已经分配给"T2"
,所以这就是析构函数插入cout
的内容。这在移动和复制情况下都会发生,这是输出中的第二Destructor TX
行。
但是,为什么当移动分配存在(并调用)时会有所不同?
区别在于何时销毁行b = B("T2")
中的临时对象。这是输出中的第一个Destructor TX
行。
复制分配后,此临时仍将保留"T2"
值,因此这就是您在析构函数中看到的内容。
移动分配后,临时不再保证包含"T2"
,而是保持有效但未指定的状态(如std::string
规范中所述),因此输出可以是任何内容。在这种情况下,它恰好是"T1"
.(根据此结果,我们可能会猜测字符串的移动赋值运算符可能是通过交换内部缓冲区实现的。此观察不是保证的行为)。
该文档指定移动后的另一个分配可以保留为任何状态。如果 B 的成员没有移动赋值,为什么不调用 T2 的时间右值(即 =B("T2")的右侧)的析构函数?
临时的析构函数被调用。临时在移动后不再处于"包含"T2"
"所描述的状态。