为啥在析构函数中抛出异常时不调用重载删除?

Posted

技术标签:

【中文标题】为啥在析构函数中抛出异常时不调用重载删除?【英文标题】:Why is an overloaded delete not called when an exception is thrown in a destructor?为什么在析构函数中抛出异常时不调用重载删除? 【发布时间】:2017-08-12 20:52:27 【问题描述】:

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

抛出异常时,为什么delete操作符中的代码没有被执行(并打印“再见”)?

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

#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

Live demo.

我看了:

throwing exceptions out of a destructor How does C++ free the memory when a constructor throws an exception and a custom new is used 及其他

但我无法找到关于 (1) 析构函数(与构造函数相反)中的异常和 (2) 重载删除究竟会发生什么的答案。

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


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

【问题讨论】:

我已经写了下面的代码[...]我刚刚遇到了这个代码 ...我很困惑。 可能是为了简化他在 Internet 上找到的内容而编写的给定代码。 我确实使用 g++ 7.1.0 和 clang++ 3.8.0 得到了“再见”。不过,我找不到描述更改内容的 gcc 错误报告。 [expr.delete] 有“[注意:无论对象的析构函数或数组的某些元素是否抛出异常,都会调用释放函数。]”。 [except.ctor] 具有“对于任何存储持续时间的类类型的对象,其初始化或销毁被异常终止... [注意:如果对象是由 new-expression 分配的,调用匹配的释放函数(如果有)以释放对象占用的存储空间。]" 不过,我很难找到规范的措辞。 这是core issue 353 和GCC bug 55635。 【参考方案1】:

standard Draft N4296 5.3.5,第 121 页说:

[expr.delete] [注:释放函数 无论对象的析构函数还是数组的某个元素是否抛出异常,都会调用。 ——尾注]

所以operator delete 必须被调用,不管析构函数抛出。

但是,从 cmets 中可以看出,一些编译器不能正确调用 operator delete。这可以通过bug编译器来解决。

错误测试:

GCC 4.8 Visual Studio 2015

【讨论】:

从标准中引用时,您应该给出与您引用的部分相关联的标签(在这种情况下为 [expr.delete]),因为这些部分可以移动并重新编号。 (这是 N4659 中的第 8.3.5 节第 7 段)。【参考方案2】:

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

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

7 delete-expression 将调用 deallocation 函数 (3.7.3.2)。

结合起来,这些子句要求将调用析构函数(或数组的析构函数)并且无条件调用释放函数。这里没有规定如果抛出异常则不调用释放函数。

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

在Draft N4296 available from open-std.org中,相同的子句扩展如下:(记忆中官方标准中的措辞是相同的,但我现在的机器上没有副本) (强调我的)

6 如果 delete-expression 的操作数的值不是空指针值,则 delete-expression 将调用析构函数(如果有)被删除的对象或数组的元素。在一个 数组,元素将按照地址递减的顺序被销毁(也就是说,按照它们的构造函数完成的相反顺序;参见 12.6.2)。

7 如果delete-expression的操作数的值不是空指针值,那么:

(7.1) - 如果对要删除的对象的 new-expression 的分配调用未被省略且分配未被扩展 (5.3.4),则 delete-expression 应调用 deallocation 函数 (3.7.4.2)。 new-expression 的分配调用返回的值应作为第一个参数传递给释放函数。

(7.2) - 否则,如果分配被扩展或通过扩展另一个 new-expression 的分配提供,并且 delete-expression 为每个其他指针由扩展 new-expression 提供存储的 new-expression 生成的值已被评估, delete-expression 应该调用一个释放函数。扩展 new-expression 的分配调用返回的值应作为第一个参数传递给释放函数。

(7.3) - 否则,delete-expression 不会调用 释放函数 (3.7.4.2)。

否则,未指定是否调用释放函数。 [注意: 无论对象的析构函数还是数组的某些元素是否抛出异常,都会调用释放函数。结束说明 ]

最后的注释说明,即使析构函数抛出异常,也必须调用释放函数。

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

【讨论】:

如果您为我所看到的行为添加一些明确的解释(即使它是推测性的),我会更好地接受这个答案,例如可能是编译器错误,或者可能是初始措辞未明确处理这种情况的结果。【参考方案3】:

在调用删除操作符之前调用析构函数。见cppreference - delete expression

如果 expression 不是空指针,则 delete 表达式为正在销毁的对象或正在销毁的数组的每个元素(从数组的最后一个元素继续到第一个元素)调用析构函数(如果有) )。 之后,除非匹配的 new 表达式与另一个 new 表达式(C++14 起)组合,否则 delete 表达式调用释放函数,操作符 delete(对于表达式的第一个版本)或操作符 delete[](对于表达式的第二个版本)。

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

【讨论】:

我看不到对抛出析构函数的任何引用,还是我错了? 该参考不是用于抛出析构函数,而是用于带有删除表达式的操作顺序。由于这种操作顺序,在调用删除运算符的重载版本之前调用析构函数并引发异常。 您假设在执行此操作的代码中(无论是无意还是有意)没有异常处理 - 这在您引用的文本中没有说明。 cppreference.com 不是规范性参考。您引用的段落同样可以解释为无条件地在销毁之后进行释放。

以上是关于为啥在析构函数中抛出异常时不调用重载删除?的主要内容,如果未能解决你的问题,请参考以下文章

在析构函数中捕获异常

为啥我必须在析构函数中调用 MPI.Finalize() ?

glDeleteBuffers() 在析构函数调用期间崩溃

c ++在析构函数中删除向量类成员内存

析构函数为啥能释放对象内存?

如果debug调试的时候中断总是停在析构函数的delete[] p上