为什么在析构函数中引发异常时不调用重载删除

Why is an overloaded delete not called when an exception is thrown in a destructor?

本文关键字:调用 重载 删除 异常 析构函数 为什么      更新时间:2023-10-16

我已经编写了下面的代码,它重载了newdelete运算符,并在析构函数中抛出异常。

当抛出异常时,为什么delete运算符中的代码没有执行(并打印"再见")?

如果不应该执行,(如何)释放内存?是否调用了其他delete运算符之一?重载其中一个会导致相应的代码被执行吗?或者,内存根本没有被释放,因为失败的破坏意味着它可能不应该被释放?

#include <iostream>
using namespace std;
class A
{
public:
A() { }
~A() noexcept(false) { throw exception(); }
void* operator new (std::size_t count)
{
cout << "hi" << endl;
return ::operator new(count);
}
void operator delete (void* ptr)
{
cout << "bye" << endl;
return ::operator delete(ptr);
}
// using these (with corresponding new's) don't seem to work either
// void operator delete (void* ptr, const std::nothrow_t& tag);
// void operator delete (void* ptr, void* place);
};
int main()
{
A* a = new A();
try
{
delete a;
}
catch(...)
{
cout << "eek" << endl;
}
return 0;
}

输出:

hi
eek

现场演示。

我看了看:

  • 从析构函数中抛出异常
  • 当构造函数抛出异常并使用自定义new时,C++如何释放内存
  • 以及其他

但我找不到确切的答案:(1)析构函数中的异常(与构造函数相反)和(2)重载删除。

我不需要关于在析构函数中抛出异常是一种糟糕做法的讲座——我只是遇到了类似的代码,我对这种行为很好奇。


如果存在标准或类似参考文献,我更希望得到该参考文献支持的答案。

标准草案N4296 5.3.5,第121页称:

[expr.delete][注意:解除分配函数无论对象或数组的某个元素的析构函数是否引发异常,都会调用。--尾注]

因此,必须在析构函数抛出的无顺序调用operator delete

然而,正如评论中所显示的那样,一些编译器并没有正确地调用operator delete。这可以作为错误编译器来解决。

测试错误:

  • GCC 4.8
  • Visual Studio 2015

在1998年C++标准(ISO/IEC 14882第一版,1998-09-01)中,删除表达式的工作方式在第6段和第7段的"第5.3.5节删除[expr.delete]"中非常简单地说明。

6删除表达式将为要删除的对象或数组元素调用析构函数(如果有的话)。在数组的情况下,元素将按照地址递减的顺序(即其构造函数的完成顺序相反;见12.6.2)。

7删除表达式将调用解除分配函数(3.7.3.2)。

在组合中,这些子句要求将调用析构函数(或数组的析构函数),并且将无条件地调用释放函数。这里没有关于在引发异常时不调用deallocation函数的规定。

在1998年的标准中,语言律师和编译器开发人员可能会为争论与我上面所说的不同的解释而感到高兴。幸运的是,在后来的标准中,情况更加明确。。。

在open-std.org上提供的草案N4296中,相同的条款扩展如下:(据记忆,官方标准中的措辞相同,但我目前的机器上没有副本)
(强调我的)

6如果删除表达式的操作数值不是空指针值,则

的删除表达式7如果删除表达式的操作数值不是空指针值,则:

(7.1)-如果要删除的对象的新表达式的分配调用没有被省略,并且分配没有被扩展(5.3.4)delete表达式应调用释放函数(3.7.4.2)。新表达式allocation调用返回的值应作为第一个参数传递给释放函数。

(7.2)-否则,如果分配是扩展的,或者是通过扩展另一个新表达式的分配而提供的,并且对于具有由扩展的new表达式

提供的存储的new表达式产生的每一个其他指针值,delete表达式elete表达式应调用一个解除分配函数。扩展新表达式的分配调用返回的值应作为第一个参数传递给deallocation函数。

(7.3)-否则,删除表达式将不会调用解除分配功能(3.7.4.2)。

否则,将不指定是否调用deallocation函数。注意:无论对象或数组的某个元素的析构函数是否抛出异常,都会调用deallocation函数。--结束注释]

结尾的注释指出,即使析构函数抛出异常,也必须调用释放函数。

我不确定是哪种标准的演变首先阐明了问题,但基于上述内容,条款可能会保留在第5.3.5节中(标签[expr.delete])。

在调用delete运算符之前调用析构函数。请参阅cppreference-删除表达式

如果表达式不是空指针,则delete表达式会为要销毁的对象或要销毁的数组的每个元素调用析构函数(如果有)(从数组的最后一个元素到第一个元素)。之后,除非匹配的新表达式与另一个新表达式组合(由于C++14),否则delete表达式将调用释放函数,即运算符delete(对于表达式的第一个版本)或运算符delete[](针对表达式的第二个版本)。

由于这种操作顺序,在调用重载版本的delete运算符之前,会调用析构函数并引发异常。