手动调用析构函数总是糟糕设计的标志吗?
Posted
技术标签:
【中文标题】手动调用析构函数总是糟糕设计的标志吗?【英文标题】:Is calling destructor manually always a sign of bad design? 【发布时间】:2013-01-06 21:33:21 【问题描述】:我在想:他们说如果你手动调用析构函数 - 你做错了什么。但总是这样吗?有没有反例?需要手动调用或难以/不可能/不切实际避免调用的情况?
【问题讨论】:
在调用 dtor 之后如何释放对象而不再次调用它? @peachykeen:您可以调用placementnew
来初始化一个新对象来代替旧对象。通常不是一个好主意,但并非闻所未闻。
查看包含“总是”和“从不”这两个词的“规则”,这些词不是直接来自规范的怀疑:在大多数情况下,教他们的人想隐藏你的东西你应该知道,但他不知道怎么教。就像成年人回答孩子关于性的问题一样。
我认为在使用放置技术stroustrup.com/bs_faq2.html#placement-delete 构造对象的情况下操作没问题(但这是相当低级的事情,只有在您优化软件时才使用,即使在这样的级别)
【参考方案1】:
所有答案都描述了具体情况,但有一个普遍的答案:
每次您需要销毁 object(在 C++ 意义上)而不释放对象所在的 memory 时,您都显式调用 dtor。
这通常发生在内存分配/释放与对象构造/销毁独立管理的所有情况下。在这些情况下,构造是通过 placement 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::vector
使用的默认std::allocator
:元素在push_back
期间在vector
中构造,但内存是按块分配的,因此它预先存在元素构造。因此,vector::erase
必须销毁元素,但不一定会释放内存(特别是如果新的 push_back 必须很快发生......)。
这是严格的 OOP 意义上的“糟糕设计”(您应该管理对象,而不是内存:对象需要内存的事实是“事件”),它是“低级编程”中的“好设计”,或者在某些情况下默认operator new
购买的内存不是从“免费存储”中获取的。
如果它在代码周围随机发生,这是一个糟糕的设计,如果它在本地发生在专门为此目的设计的类中,这是一个好的设计。
【讨论】:
【参考方案2】:如果对象是使用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 创建一个新对象。另外,当union被销毁时,如果需要销毁,需要显式调用当前对象的析构函数。
【讨论】:
不要说“使用operator new
的重载形式”,正确的短语是“使用placement new
”。
@RemyLebeau:嗯,我想澄清一下,我不仅在谈论 operator new(std::size_t, void*)
(和数组变体),而且还谈论 operator new()
的所有重载版本。
当你想复制一个对象在它里面做一个操作而不在操作计算的时候改变它怎么办? temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
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 原则的使用。如果在这种情况下使用移动operator=()
,它甚至可能比使用交换更好。 YMMV。
@Adrian:调用析构函数并重新创建对象很容易改变对象的类型:它将重新创建一个静态类型的对象,但动态类型可能不同。当类具有 virtual
函数(virtual
函数不会被重新创建)并且对象只是部分[重新]构造时,这实际上是一个问题。【参考方案3】:
不,您不应该显式调用它,因为它会被调用两次。一次用于手动调用,另一次用于声明对象的范围结束。
例如。
Class c;
c.~Class();
如果你真的需要执行相同的操作,你应该有一个单独的方法。
有一个specific situation,您可能希望在一个动态分配的对象上调用析构函数,该对象的位置为new
,但这听起来您永远不需要。
【讨论】:
【参考方案4】:不,视情况而定,有时它是合法且好的设计。
要了解为什么以及何时需要显式调用析构函数,让我们看看“new”和“delete”会发生什么。
为了动态创建一个对象,T* t = new T;
在后台: 1. sizeof(T) 内存被分配。 2.调用T的构造函数初始化分配的内存。运算符 new 做了两件事:分配和初始化。
在底层销毁对象delete t;
: 1. T 的析构函数被调用。 2. 为该对象分配的内存被释放。操作符 delete 也做了两件事:销毁和释放。
编写构造函数进行初始化,编写析构函数进行销毁。当您显式调用析构函数时,只完成了析构,但没有解除分配。
因此,显式调用析构函数的合法用途可能是,“我只想销毁对象,但我不(或不能)释放内存分配(还)。”
这方面的一个常见示例是为某些对象池预先分配内存,否则这些对象必须动态分配。
当创建一个新对象时,你会从预分配的池中获取内存块并进行“新的放置”。完成对象后,您可能需要显式调用析构函数来完成清理工作(如果有)。但是您实际上不会像操作员 delete 那样释放内存。相反,您将块返回到池中以供重用。
【讨论】:
【参考方案5】:正如常见问题解答中所引用的,you should call the destructor explicitly when using placement new。
这是您唯一一次显式调用析构函数。
我同意,虽然这很少需要。
【讨论】:
【参考方案6】:任何时候您需要将分配与初始化分开, 你需要放置新的和显式的析构函数调用 手动。今天,它很少需要,因为我们有 标准容器,但如果你必须实现一些新的排序 容器,你需要它。
【讨论】:
【参考方案7】:有些情况是必要的:
在我处理的代码中,我在分配器中使用显式析构函数调用,我实现了简单的分配器,它使用新位置将内存块返回给 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);
在 allocate() 中还进行了分配,在 deallocate() 中进行了内存释放,使用平台特定的 alloc 和 dealloc 机制。此分配器用于绕过 doug lea malloc 并直接使用例如 Windows 上的 LocalAlloc。
【讨论】:
【参考方案8】:我发现 3 次我需要这样做:
在由 memory-mapped-io 或共享内存创建的内存中分配/释放对象 使用 C++ 实现给定的 C 接口时(是的,不幸的是,今天仍然会发生这种情况(因为我没有足够的影响力来更改它)) 在实现分配器类时【讨论】:
【参考方案9】:这个呢? 如果构造函数抛出异常,则不会调用析构函数,所以我必须手动调用它来销毁异常之前在构造函数中创建的句柄。
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);
;
【讨论】:
这似乎是有问题的:当您的构造函数运行时,您不知道(或可能不知道)对象的哪些部分已经构建,哪些没有。因此,例如,您不知道要为哪些子对象调用析构函数。或者构造函数分配的哪些资源要释放。 @VioletGiraffe 如果子对象是在堆栈上构建的,即不是使用“new”,它们将被自动销毁。否则,您可以在析构函数中销毁它们之前检查它们是否为 NULL。与资源相同 您在此处编写ctor
的方式是错误的,原因正是您自己提供的:如果资源分配失败,则说明清理存在问题。 “演员”不应该打电话给this->~dtor()
。 dtor
应该在 constructed 对象上调用,在这种情况下,该对象尚未构造。无论发生什么,ctor
都应该处理清理工作。在ctor
代码中,您应该使用std::unique_ptr
之类的工具来为您处理自动清理,以防出现异常情况。更改类中的 HANDLE h1, h2
字段以支持自动清理也是一个好主意。
这意味着,ctor 应该看起来像: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;
等,那么您甚至可能根本不需要dtor
。
cleanupGuard1
和cleanupGuard2
的实现取决于相关xxxToCreate
返回什么以及相关xxxxToDestroy
采用什么参数。如果它们很简单,你甚至可能不需要写任何东西,因为通常std::unique_ptr<x,deleter()>
(或类似的)在这两种情况下都可以为你解决问题。【参考方案10】:
我从未遇到过需要手动调用析构函数的情况。我似乎记得 Stroustrup 声称这是不好的做法。
【讨论】:
你是对的。但我使用了新的展示位置。我能够在除析构函数之外的方法中添加清理功能。析构函数在那里,因此可以在删除时“自动”调用它,当您手动想要销毁但不释放时,您可以简单地编写一个“onDestruct”,不是吗?我很想知道是否有对象必须在析构函数中进行销毁的示例,因为有时您需要删除,而其他时候您只想破坏而不是释放.. 即使在这种情况下,您也可以从析构函数中调用 onDestruct() - 所以我仍然没有看到手动调用析构函数的案例。 @JimBalter:C+
的创建者@☺
@MarkKCowan:什么是 C+?应该是C++【参考方案11】:
找到另一个必须手动调用析构函数的示例。假设您已经实现了一个类似变体的类,该类包含几种类型的数据之一:
struct Variant
union
std::string str;
int num;
bool b;
;
enum Type Str, Int, Bool type;
;
如果Variant
实例持有std::string
,而现在您要为联合分配不同的类型,则必须首先销毁std::string
。 The compiler will not do that automatically.
【讨论】:
【参考方案12】:我还有另一种情况,我认为调用析构函数是完全合理的。
当编写“重置”类型的方法来将对象恢复到其初始状态时,调用析构函数来删除正在重置的旧数据是完全合理的。
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.
;
【讨论】:
在这种情况下和在析构函数中声明一个特殊的cleanup()
方法会不会看起来更简洁?
只在两种情况下调用的“特殊”方法?当然......这听起来完全正确(/讽刺)。方法应该是通用的,并且可以在任何地方调用。当你想删除一个对象时,调用它的析构函数并没有错。
在这种情况下你不能显式调用析构函数。无论如何,你必须实现一个赋值运算符。以上是关于手动调用析构函数总是糟糕设计的标志吗?的主要内容,如果未能解决你的问题,请参考以下文章