【问题标题】:Default move assignment calls destructor, copy assignment doesn't默认移动赋值调用析构函数,复制赋值不调用
【发布时间】:2019-03-27 11:50:14
【问题描述】:

我观察到一个奇怪的行为,我不太了解析构函数和(默认)复制和移动分配。

假设我有一个类B 具有默认所有内容,还有一个类Test 具有自定义析构函数、默认复制分配和(可能)默认移动分配。

然后我们创建一个B 的实例,将其分配给一个变量,并使用赋值替换为一个新实例(其中右侧是右值)。

我觉得有两件事很奇怪,我在文档中看不到它们的原因。

  1. Test 没有move assignment(因此调用它的复制赋值)时,T1 对象的析构函数不会被显式调用。我认为在这种情况下,惯用的做法是将资源作为copy assignment 的一部分进行清理。但是,当move assignment 存在(并被调用)时,为什么会有所不同?如果它在那里 Test 的析构函数被显式调用(?由操作员)。
  2. 文档指定移动分配后的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 0\n";

   return 0;
}


int main() {
    B b("T1");
    std::cout << "fce call\n";
    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

【问题讨论】:

  • 使用move,您的行为未定义,因为您的析构函数访问处于未指定状态的_name
  • 看起来std::string 的实现在移动时进行了交换。因此Destructor T1 实际上是B("T2") 被移出后的析构函数。我这边的测试产生了Destructor (空字符串)。
  • @Eljay 未指定 但有效 状态,因此它没有达到 UB。
  • 不是说它是UB,只是说它是未指定的状态。正如昆汀所澄清的,未指定但有效的状态。在那种状态下,几乎没有人可以使用它。它可以被分配,也可以被破坏。
  • @Eljay "不是说它是 UB,只是说它是未指定的状态。" 但是,你说 "move,你有未定义的行为" 那是你说的,还是你没说的?

标签: c++ move


【解决方案1】:

默认移动赋值调用析构函数,复制赋值不调用

这两个赋值都会导致临时 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"”所描述的状态。

【讨论】:

    猜你喜欢
    • 2016-09-13
    • 1970-01-01
    • 2013-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-10
    • 1970-01-01
    • 2010-12-04
    相关资源
    最近更新 更多