析构函数抛出异常会发生什么
What happens when a destructor throws

原始链接: https://www.sandordargo.com/blog/2026/04/01/when-a-destructor-throws

## 析构函数与异常:C++ 的关键理解 人工智能代码生成能力的提升凸显了对核心编程概念的*深入*理解的重要性,而不仅仅是编写代码的能力。令人惊讶的是,即使在资深开发者中,也存在一个常见的知识差距,那就是 C++ 析构函数抛出异常时会发生什么。 析构函数对于 RAII(资源获取即初始化)至关重要,确保即使在发生错误时也能释放资源。然而,析构函数具有有限的错误报告选项。虽然它们*可以*抛出异常,但通常不建议这样做。 自 C++11 以来,析构函数默认情况下是 `noexcept(true)`。如果一个 `noexcept` 析构函数抛出异常,程序会立即调用 `std::terminate`。将析构函数标记为 `noexcept(false)` 允许异常传播,*除非* 另一个异常已经在进行解退栈——在这种情况下,*始终* 会调用 `std::terminate`。 本质上,析构函数在解退栈期间抛出异常是无法恢复的。因此,最佳实践是完全避免在析构函数中抛出异常,而倾向于使用日志记录、错误标志或存储失败状态以实现更安全的资源管理。理解这种行为对于编写健壮且可预测的 C++ 代码至关重要。

## Hacker News 讨论:C++ 中析构函数抛出异常 一篇关于 C++ 析构函数抛出异常时会发生什么情况的文章在 Hacker News 上引发了争论。核心讨论围绕着对这种特定且可能罕见情况的了解是否对资深 C++ 开发者至关重要。 一些人认为这无关紧要的“语言版本特定细节”,在需要时可以轻松查阅,并且浪费面试时间。另一些人则认为这是测试 C++ 专业知识的有效方法,强调理解析构函数是该语言的基础——引用像《Effective C++》这样涵盖该主题的书籍。 对话涉及对象销毁过程中异常处理的复杂性,特别是双重异常场景的灾难性后果(析构函数清理期间发生的异常导致 `std::terminate`)。 许多人同意*避免*在析构函数中抛出异常是最佳实践,但对于深入理解如果发生异常的后果的重要性存在分歧。 一些评论员也指出,这个级别的细节对于编译器/调试器开发者比典型的应用程序工程师更相关。
相关文章

原文

Recently I wrote about the importance of finding joy in our jobs on The Dev Ladder. Mastery and deep understanding are key elements in finding that joy, especially now that generating code is cheap and increasingly done better by AI than by us.

Then a memory surfaced. I frequently ask during interviews — as part of a code review exercise — what happens when a destructor throws. Way too many candidates, even those interviewing for senior positions, cannot answer the question. Most say it’s bad practice, but cannot explain why. Some say the program might terminate. Getting an elaborate answer is rare.

I’m not saying it’s a dealbreaker, but it definitely doesn’t help.

Let’s see what actually happens.

The role of a destructor

A destructor is the key to implementing the RAII idiom. RAII matters because after you acquire a resource, things might go south. A function might need to return early, or it might throw. Making sure resources are released is cumbersome, and the cleanest way to achieve it is to wrap both acquisition and release in an object that handles this automatically.

But what if the release itself is not successful?

Destructors have no return value, so error reporting is limited. Typical options include logging, storing error state, or (discouraged) throwing.

Why did I mark throwing an exception discouraged?

What happens when an exception is thrown

When an exception is thrown, runtime stack unwinding starts.

First, automatic objects in the current scope are destroyed in reverse order, with their destructors executed.

If another exception is thrown during unwinding, std::terminate is called.

If a matching exception handler is found, execution continues there.

What if a destructor throws with no other active exception?

Let’s start with a simple example where no exception handling is ongoing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://godbolt.org/z/zn9b19jao
#include <iostream>

struct A {
    ~A() {
        std::cout << "Destructor\n";
        throw std::runtime_error("boom");
    }
};

int main() {
    try {
        A a;
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
}

Let’s go step by step:

  • we enter the try block
  • A a is constructed
  • a’s scope ends and ~A() is called
  • A::~A() throws

And then…

We have to stop for a second and recall an important rule:

Since C++11, destructors are implicitly noexcept(true) unless declared otherwise or a base or member destructor can throw.

As an exception would leave our noexcept destructor, the noexcept guarantee is violated, so std::terminate is called. The catch block is never reached.

What if we want the destructor to be allowed to throw?

Let’s update the example and mark the destructor throwable with noexcept(false):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://godbolt.org/z/KKhErsv6Y
#include <iostream>

struct A {
    ~A() noexcept(false) {
        std::cout << "Destructor\n";
        throw std::runtime_error("boom");
    }
};

int main() {
    try {
        A a;
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
}

In this case, the exception propagates normally and the catch block intercepts it.

So a destructor can throw as long as it’s explicitly marked noexcept(false).

I said can, not should. And there’s a critical caveat.

What if a destructor throws while another exception is active?

What if a destructor throws during stack unwinding? Let’s update our example slightly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// https://godbolt.org/z/a1n75Tb4c
#include <iostream>

struct A {
    ~A() noexcept(false) {
        std::cout << "Destructor throwing\n";
        throw std::runtime_error("boom");
    }
};

int main() {
    try {
        A a;
        throw std::runtime_error("original");
    } catch (...) {
        std::cout << "caught\n";
    }
}

Let’s go through what happens step by step:

  • we enter the try block
  • throw std::runtime_error("original") is thrown and stack unwinding starts
  • as part of the unwinding, local objects are destroyed, so A::~A() is called
  • A::~A() throws a second exception while the original is still active — std::terminate() is called

This termination is mandated by the C++ standard and no catch block is reached.

The rule exists because otherwise the runtime would need to track multiple simultaneous propagations, which would be complex and almost certainly ambiguous.

Conclusion

When a destructor throws, the outcome depends on context.

If no other exception is active and the destructor is explicitly marked noexcept(false), the exception propagates normally and can be caught. But this is the exception — in both senses of the word. Since C++11, destructors are implicitly noexcept(true), so a throwing destructor will call std::terminate by default, bypassing any catch block entirely.

The real danger is the second scenario: a destructor throwing while stack unwinding is already in progress. Even with noexcept(false), this always calls std::terminate. You cannot let an exception escape a destructor during unwinding — the standard simply does not allow it.

This is why the conventional wisdom holds: destructors should not throw. If resource release fails, the alternatives — logging the error, setting an error flag, or storing the failure state for later inspection — are far safer than propagating exceptions out of a destructor. The noexcept(false) opt-out exists, but it requires careful handling and should only be used when you can guarantee that the destructor is never called during stack unwinding.

Have you ever encountered a codebase that threw from destructors deliberately? How was the error handling structured?

Connect deeper

If you liked this article, please

联系我们 contact @ memedata.com