从赋值运算符调用析构函数是否有任何意想不到的后果?

Are there any unexpected consequences of calling a destructor from the assignment operator?

提问人:Matt 提问时间:1/23/2015 更新时间:1/23/2015 访问量:87

问:

例如:

class Foo : public Bar
{
    ~Foo()
    {
        // Do some complicated stuff
    }

    Foo &operator=(const Foo &rhs)
    {
        if (&rhs != this)
        {
            ~Foo(); // Is this safe?

            // Do more stuff
        }
    }
}

在继承和其他类似事情上显式调用析构函数是否有任何意想不到的后果?

有什么理由将析构函数代码抽象为函数并调用它吗?void destruct()

C++ 析构函数 赋值运算符

评论

3赞 juanchopanza 1/23/2015
我想说的是,这样做不会产生意外后果的案例数量非常有限。除非你预料到意外。
2赞 cdhowie 1/23/2015
我更倾向于提供一种方法(私有的,如果它没有意义成为公共接口的一部分)并从两个地方调用它。即使在特定情况下技术上没有必要,它看起来也不会那么奇怪,并且不太可能绊倒下一个必须维护你的代码的人clear()
0赞 excalibur 1/24/2015
您是否担心在分配新值之前释放资源?如果是这样,我宁愿使用 compare-swap 成语或编写一个清理函数

答:

5赞 Sebastian Redl 1/23/2015 #1

在最简单的情况下,调用析构函数是一个坏主意,而当代码变得稍微复杂一点时,这是一个可怕的主意。

最简单的情况是这样的:

class Something {
public:
    Something(const Something& o);
    ~Something();
    Something& operator =(const Something& o) {
        if (this != &o) {
            // this looks like a simple way of implementing assignment
            this->~Something();
            new (this) Something(o); // invoke copy constructor
        }
        return *this;
    }
};

这是个坏主意。如果复制构造函数抛出,则只剩下原始内存 - 那里没有对象。只是,在赋值运算符之外,没有人注意到。

如果继承发挥作用,事情就会变得更糟。假设实际上是一个具有虚拟析构函数的基类。派生类的函数都是用默认值实现的。在这种情况下,派生类的赋值运算符将执行以下操作:Something

  1. 它将调用自己的基本版本(赋值运算符)。
  2. 赋值运算符调用析构函数。这是一个虚拟通话。
  3. 派生的析构函数销毁派生类的成员。
  4. 基析构函数销毁基类的成员。
  5. 赋值运算符调用复制构造函数。这不是虚拟的;它实际上构造了一个基本对象。(如果基类不是抽象的。如果是,则代码将无法编译。现在,您已将派生对象替换为基本对象。
  6. 复制构造函数构造基的成员。
  7. 派生赋值运算符对派生类的成员进行成员赋值。这些已被摧毁而不是重新创建。

在这一点上,你有多个UB实例堆积在一起,在一堆光荣的完全混乱中。

所以是的。别这样。

评论

0赞 Matt 1/24/2015
需要明确的是,我也从未打算调用复制构造函数。另外,如果复制构造函数抛出,赋值运算符不会直接传递抛出吗?
0赞 Sebastian Redl 1/24/2015
@Matt 这只会让情况变得更糟,因为即使在非错误的情况下,您在分配后也只有原始内存。需要明确的是:从逻辑上讲,调用析构函数会结束对象的生存期。对对象的任何进一步操作都是未定义的行为,因为那里不再有对象。您可以使用该内存执行的唯一操作(并且必须执行,因为编译器可能仍会在某个时候隐式调用析构函数)是使用 placement new 创建新对象。
0赞 Sebastian Redl 1/24/2015
@Matt 至于扔,是的,它会的。你的观点是什么?如果你在任务之外发现了异常,你仍然有一些原始记忆隐藏在你摧毁的可怜物体的死壳下。
0赞 Matt 1/24/2015
好吧,那么我想我必须抓住它并在赋值运算符本身中修复它。与放置新抛出的任何其他时间没有什么不同。它仍然与我关于析构函数的问题无关。
0赞 Sebastian Redl 1/24/2015
@Matt 如果默认构造函数也抛出怎么办?那你怎么解决呢?它与析构函数有关,因为我告诉你为什么你永远不应该这样称呼它。
0赞 hlide 1/23/2015 #2

绝对不行。

void f()
{
    Foo foo, fee;
    foo = fee; <-- a first foo.~Foo will be called here !
} <-- a second foo.~Foo will be called here and fee.~Foo as well !

如您所见,您有 3 次对析构函数的调用,而不是预期的 2 次调用。

不应在构造函数或非静态方法中使用 *self-* 析构函数。

评论

0赞 Matt 1/24/2015
那一秒将是安全的,因为该物体在第一次被破坏后会立即重建。此外,我编写代码的方式,调用析构函数两次无论如何都是安全的。foo.~Foo
0赞 Ulrich Eckhardt 1/24/2015
从形式上讲,它仍然是未定义的行为,AFAIK。无论如何,如果重建抛出异常,你就是 SOL,那么你就有一个被摧毁的物体,并带来所有令人讨厌的后果。创建一个临时的,取而代之的是,即延迟销毁实际有效载荷,直到您可以保证异常安全。this
0赞 Matt 1/24/2015
@UlrichEckhardt,我是重建它的人,所以我可以处理任何异常。
0赞 hlide 1/26/2015
尽管如此,还是有很多限制需要注意,比如不分享真正的指针。这样做可能会导致一些令人讨厌的错误,特别是对于其他人来说,他们可能会在产品开发中取代您并且不够聪明,不会搞砸。