手动调用析构函数总是表明设计不好吗?


83

我在想:他们说如果您手动调用析构函数-您做错了什么。但是,总是这样吗?有没有反例?需要手动调用它或很难/不可能/不切实际地避免它的情况?


调用dtor之后如何重新分配对象,而不再次调用它?
ssube

2
@peachykeen:您将调用放置new来初始化一个新对象来代替旧对象。通常这不是一个好主意,但这并不是闻所未闻的。
D.Shawley

14
看“规则”中包含“总是”和“从不”这两个词,这些词并非直接来自带有可疑性的规范:在大多数情况下,教他们的人想向您隐藏您应该知道的东西,但他却不知道知道怎么教。就像成年人回答孩子有关性的问题一样。
Emilio Garavaglia 2013年

我认为在采用放置技术stroustrup.com/bs_faq2.html#placement-delete构造对象的情况下很好(但这是一个较低级别的东西,即使在这样的水平上也仅在优化软件时才使用)
bruziuz

Answers:


94

如果对象是使用的重载形式构造的,则需要手动调用析构函数operator new(),除非使用“ std::nothrow”重载:

T* t0 = new(std::nothrow) T();
delete t0; // OK: std::nothrow overload

void* buffer = malloc(sizeof(T));
T* t1 = new(buffer) T();
t1->~T(); // required: delete t1 would be wrong
free(buffer);

但是,像上面显式调用析构函数那样,在较低级别上进行外部内存管理设计不良的标志。可能实际上,这不仅是错误的设计,而且是完全错误的(是的,在赋值运算符中使用显式析构函数后再进行复制构造函数调用错误的设计,并且很可能是错误的)。

在C ++ 2011中,还有一个使用显式析构函数调用的原因:使用广义联合时,有必要在更改表示对象的类型时显式销毁当前对象并使用placement new创建一个新对象。另外,在销毁并集时,如果需要销毁,则必须显式调用当前对象的析构函数。


26
与其说“使用重载形式operator new”,不如说是“使用placement new”。
Remy Lebeau

5
@RemyLebeau:好吧,我想澄清的是,我不仅在谈论operator new(std::size_t, void*)(以及数组的变体),而且还谈论了所有的重载版本operator new()
DietmarKühl2013年

当您要复制对象以在其中进行操作而不在计算过程中更改对象时该怎么办?temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
Jean-Luc Nacif Coelho

yes, using an explicit destructor followed by a copy constructor call in the assignment operator is a bad design and likely to be wrong。为什么这样说 我认为,如果析构函数是微不足道的或接近微不足道的,则其开销最小,并且会增加DRY原理的使用。如果在这种情况下使用move operator=(),它甚至可能比使用swap更好。YMMV。
阿德里安

1
@Adrian:调用析构函数并重新创建对象非常容易地更改对象的类型:它将使用分配的静态类型重新创建对象,但动态类型可能有所不同。当类具有virtual函数时(这virtual将不会重新创建函数),否则实际上就是一个问题,否则该对象仅是部分[重新]构造的。
DietmarKühl,

101

所有答案都描述了具体情况,但是有一个通用答案:

每当您只需要销毁对象(从C ++角度而言)而不释放对象所驻留的内存时,都可以显式调用dtor 。

这通常发生在所有与对象构造/销毁无关地管理内存分配/释放的情况下。在那些情况下,构建是通过在现有内存块上放置new来进行的,而销毁是通过显式dtor调用发生的。

这是原始示例:

{
  char buffer[sizeof(MyClass)];

  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }
  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }

}

另一个值得注意的示例是std::allocator当由时使用的默认值std::vector:元素是在vector期间构造的push_back,但是内存是按块分配的,因此它预先存在元素构造。因此,vector::erase必须销毁元素,但不一定要取消分配内存(特别是如果必须很快发生新的push_back ...)。

从严格的OOP角度来看,这是“错误的设计”(您应该管理对象,而不是内存:事实上,需要内存的对象是一个“事件”),在“低级编程”中或在内存为零的情况下,它是“良好的设计”。不从默认operator new购买的“免费商店”中提取。

如果它随机出现在代码周围是不好的设计,如果它是专门针对该目的设计的类在本地发生则是好的设计。


8
只是好奇为什么这不是公认的答案。
弗朗西斯·库格勒

11

不,您不应该明确地调用它,因为它将被调用两次。一次用于手动调用,另一次用于声明对象的范围结束。

例如。

{
  Class c;
  c.~Class();
}

如果确实需要执行相同的操作,则应该使用单独的方法。

特定情况下,您可能希望在具有放置位置的动态分配对象上调用析构函数,new但是听起来并不需要。


11

不,要视情况而定,有时这是合法且良好的设计。

为了理解为什么以及何时需要显式调用析构函数,让我们看看“ new”和“ delete”发生了什么。

要动态创建对象,请执行 T* t = new T;以下操作:1.分配了sizeof(T)内存。2.调用T的构造函数以初始化分配的内存。new运算符执行两件事:分配和初始化。

破坏delete t;引擎盖下的对象:1.调用T的析构函数。2.释放为该对象分配的内存。操作员删除还做两件事:销毁和释放。

编写构造函数进行初始化,编写析构函数进行销毁。当您显式调用析构函数时,仅会完成销毁,而不会进行释放

因此,显式调用析构函数的合法使用可能是:“我只想破坏对象,但是我(尚未)不能(或者不能)释放内存分配。”

一个常见的例子是为某些对象池预分配内存,否则必须动态分配这些对象。

创建新对象时,您将从预分配的池中获取内存块并进行“新放置”。处理完对象后,您可能希望显式调用析构函数以完成清理工作(如果有)。但是您实际上不会像操作员删除操作那样实际释放内存。取而代之的是,您将块返回池中以进行重用。



6

每当您需要将分配与初始化分开时,都需要手动放置析构函数的新的和显式的调用。如今,由于我们拥有标准的容器,因此几乎没有必要,但是如果您必须实现某种新型的容器,则将需要它。


3

在某些情况下有必要时:

在我工作的代码中,我在分配器中使用显式析构函数调用,我实现了简单的分配器的实现,该分配器使用new放置将内存块返回到stl容器。在销毁中,我有:

  void destroy (pointer p) {
    // destroy objects by calling their destructor
    p->~T();
  }

在构造时:

  void construct (pointer p, const T& value) {
    // initialize memory with placement new
    #undef new
    ::new((PVOID)p) T(value);
  }

使用平台特定的alloc和dealloc机制,在allocate()中完成分配,并在deallocate()中完成内存释放。该分配器用于绕过doug lea malloc,并直接在Windows上使用例如LocalAlloc。


1

我发现3次需要这样做的场合:

  • 在由memory-mapped-io或共享内存创建的内存中分配/取消分配对象
  • 使用C ++实现给定的C接口时(是的,不幸的是,今天仍然会发生这种情况(因为我没有足够的影响力来更改它))
  • 在实现分配器类时

1

我从未遇到过需要手动调用析构函数的情况。我似乎还记得,甚至Stroustrup也声称这是不好的做法。


1
你是对的。但我使用了新的展示位置。我可以用析构函数以外的方法添加清除功能。析构函数在那里,因此当删除某个析构函数时可以“自动”调用它,而当您手动想分解但不解除分配时,您可以简单地编写一个“ onDestruct”,不是吗?我想知道是否存在某些示例,其中某个对象必须在析构函数中进行销毁,因为有时您需要删除,而有时候您只想
分解

即使在那种情况下,您也可以从析构函数中调用onDestruct(),因此我仍然看不到手动调用析构函数的情况。
Lieuwe

4
@JimBalter:C+ ☺的创作者
马克·科恩

@MarkKCowan:什么是C +?应该是C ++
Destructor

1

那这个呢?
如果从构造函数中抛出异常,则不会调用Destructor,因此我必须手动调用它以销毁在异常之前在构造函数中创建的句柄。

class MyClass {
  HANDLE h1,h2;
  public:
  MyClass() {
    // handles have to be created first
    h1=SomeAPIToCreateA();
    h2=SomeAPIToCreateB();        
    try {
      ...
      if(error) {
        throw MyException();
      }
    }
    catch(...) {
      this->~MyClass();
      throw;
    }
  }
  ~MyClass() {
    SomeAPIToDestroyA(h1);
    SomeAPIToDestroyB(h2);
  }
};

1
这似乎是有问题的:当构造函数发生拖尾时,您不知道(或可能不知道)对象的哪些部分已经构造而哪些没有。因此,例如,您不知道要针对哪些子对象调用析构函数。或由构造函数分配的哪些资源要取消分配。
紫罗兰色长颈鹿

@VioletGiraffe如果子对象是在堆栈上构造的,即不带有“ new”,则它们将被自动销毁。否则,您可以在销毁器中销毁它们之前检查它们是否为NULL。与资源相同
CITBL

ctor正是由于您自己提供的原因,您在此处编写方法是错误的:如果资源分配失败,则清理存在问题。“ ctor”不应致电this->~dtor()dtor应该在构造对象上调用,在这种情况下,该对象尚未构造。无论发生什么情况,ctor都应处理清除。在ctor代码内部,您应该使用utils之类的工具std::unique_ptr来在发生异常时为您自动清理。更改HANDLE h1, h2类中的字段以支持自动清除也是一个不错的主意。
quetzalcoatl

这意味着,在构造函数应该是这样的:MyClass(){ cleanupGuard1<HANDLE> tmp_h1(&SomeAPIToDestroyA) = SomeAPIToCreateA(); cleanupGuard2<HANDLE> tmp_h2(&SomeAPIToDestroyB) = SomeAPIToCreateB(); if(error) { throw MyException(); } this->h1 = tmp_h1.release(); this->h2 = tmp_h2.release(); }就是这样。无需冒险进行手动清理,也无需将句柄存储在部分构造的对象中,直到一切安全为止。如果您HANDLE h1,h2将类更改为cleanupGuard<HANDLE> h1;etc,那么您甚至根本不需要dtor
quetzalcoatl

实施cleanupGuard1cleanupGuard2取决于什么做了相关的xxxToCreate回报,什么参数做了相关xxxxToDestroy服食。如果它们很简单,您甚至可能不需要编写任何东西,因为经常会发现std::unique_ptr<x,deleter()>(或类似的东西)可以在两种情况下为您解决问题。
quetzalcoatl

-2

找到了另一个示例,在该示例中,您将必须手动调用析构函数。假设您实现了一个类似变体的类,其中包含几种数据类型之一:

struct Variant {
    union {
        std::string str;
        int num;
        bool b;
    };
    enum Type { Str, Int, Bool } type;
};

如果Variant实例持有一个std::string,现在您要为该联合分配其他类型,则必须销毁第std::string一个。编译器不会自动执行此操作


-4

在另一种情况下,我认为调用析构函数是完全合理的。

在编写“ Reset”类型的方法以将对象恢复到其初始状态时,调用Destructor删除要重置的旧数据是完全合理的。

class Widget
{
private: 
    char* pDataText { NULL  }; 
    int   idNumber  { 0     };

public:
    void Setup() { pDataText = new char[100]; }
    ~Widget()    { delete pDataText;          }

    void Reset()
    {
        Widget blankWidget;
        this->~Widget();     // Manually delete the current object using the dtor
        *this = blankObject; // Copy a blank object to the this-object.
    }
};

1
如果cleanup()在这种情况下以及在析构函数中声明了要调用的特殊方法,它看起来是否更干净?
紫罗兰色长颈鹿

仅在两种情况下调用的“特殊”方法?当然...听起来完全正确(/讽刺)。方法应该是通用的,并且可以在任何地方调用。当您要删除一个对象时,调用它的析构函数没什么错。
abelenky

4
在这种情况下,您不得显式调用析构函数。无论如何,您都必须实现一个赋值运算符。
雷米
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.