C++ 使用 RAII 和抛出的析构函数
C++ using RAII with destructor that throws
假设我有 RAII class:
class Raii {
Raii() {};
~Raii() {
if (<something>) throw std::exception();
}
};
如果我有这个功能:
void foo() {
Raii raii;
if (something) {
throw std::exception();
}
}
这很糟糕,因为在清除第一个异常时我们可以再次抛出,这将终止进程。
我的问题是 - 将 raii 用于清理可能抛出的代码的好的模式是什么?
例如,这是好事还是坏事 - 为什么?
class Raii {
Raii() {};
~Raii() {
try {
if (<something>) throw std::exception();
}
catch (...) {
if (!std::uncaught_exception())
throw;
}
}
};
请注意,Raii 对象始终是堆栈分配的对象 - 这不是一般的析构函数抛出问题。
C++ 几乎肯定会有一个函数来获取 C++1z 的当前异常计数(如果他们及时发布,则称为 C++17!):std::uncaught_exceptions
(注意复数 "s").此外,析构函数默认声明为 noexcept
(这意味着如果您尝试通过异常退出析构函数,则会调用 std::terminate
)。
因此,首先,将析构函数标记为抛出 (noexcept(false)
)。接下来跟踪ctor中活跃异常的数量,与dtor中的值进行比较:如果dtor中未捕获的异常多,就知道当前正处于栈展开的过程中,再次抛出会导致调用至 std::terminate
.
现在你决定你到底有多优秀以及你希望如何处理这种情况:终止程序,或者只是吞下内部异常?
如果uncaught_exception
(单数)returns为真,一个糟糕的模仿是不抛出,但这使得异常在从试图捕获和展开的展开触发的不同dtor调用时不起作用处理 你的 异常。此选项在当前 C++ 标准中可用。
In the realm of exceptions, it is fundamental that you can do nothing if your "undo/recover" action fails. You attempt an undo operation, and you move on regardless whether the undo operation succeeds or not.
这听起来可能很疯狂,但请考虑:
- 我设法 运行 内存不足并得到一个
std::bad_alloc
异常
- 我的清理代码记录了错误
- 不幸的是,写入失败(可能磁盘已满),并尝试抛出异常
我可以撤消日志写入吗?我应该试试吗?
当抛出异常时,你真正知道的是程序处于无效状态。你不应该对一些不可能的事情最终证明是可能的感到惊讶。就我个人而言,我见过更多 Alexandrescu 的建议最有意义的案例:尝试清理,但要认识到第一个异常意味着事情已经处于无效状态,因此会出现额外的故障——尤其是由第一个问题 ("error cascade")——不足为奇。试图处理它们不会有好结果。
我应该提一下,Cap'n Proto 完全按照您的建议进行:
When Cap’n Proto code might throw an exception from a destructor, it first checks std::uncaught_exception()
to ensure that this is safe. If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel.
但是,正如 Yakk 所说,在 C++11 中,析构函数默认变为 nothrow(true)
。这意味着如果你想这样做,你需要确保在 C++11 和更高版本中你将析构函数标记为 nothrow(false)
。否则,即使没有其他异常在运行中,从析构函数中抛出异常也会终止程序。请注意,"If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel."
假设我有 RAII class:
class Raii {
Raii() {};
~Raii() {
if (<something>) throw std::exception();
}
};
如果我有这个功能:
void foo() {
Raii raii;
if (something) {
throw std::exception();
}
}
这很糟糕,因为在清除第一个异常时我们可以再次抛出,这将终止进程。
我的问题是 - 将 raii 用于清理可能抛出的代码的好的模式是什么?
例如,这是好事还是坏事 - 为什么?
class Raii {
Raii() {};
~Raii() {
try {
if (<something>) throw std::exception();
}
catch (...) {
if (!std::uncaught_exception())
throw;
}
}
};
请注意,Raii 对象始终是堆栈分配的对象 - 这不是一般的析构函数抛出问题。
C++ 几乎肯定会有一个函数来获取 C++1z 的当前异常计数(如果他们及时发布,则称为 C++17!):std::uncaught_exceptions
(注意复数 "s").此外,析构函数默认声明为 noexcept
(这意味着如果您尝试通过异常退出析构函数,则会调用 std::terminate
)。
因此,首先,将析构函数标记为抛出 (noexcept(false)
)。接下来跟踪ctor中活跃异常的数量,与dtor中的值进行比较:如果dtor中未捕获的异常多,就知道当前正处于栈展开的过程中,再次抛出会导致调用至 std::terminate
.
现在你决定你到底有多优秀以及你希望如何处理这种情况:终止程序,或者只是吞下内部异常?
如果uncaught_exception
(单数)returns为真,一个糟糕的模仿是不抛出,但这使得异常在从试图捕获和展开的展开触发的不同dtor调用时不起作用处理 你的 异常。此选项在当前 C++ 标准中可用。
In the realm of exceptions, it is fundamental that you can do nothing if your "undo/recover" action fails. You attempt an undo operation, and you move on regardless whether the undo operation succeeds or not.
这听起来可能很疯狂,但请考虑:
- 我设法 运行 内存不足并得到一个
std::bad_alloc
异常 - 我的清理代码记录了错误
- 不幸的是,写入失败(可能磁盘已满),并尝试抛出异常
我可以撤消日志写入吗?我应该试试吗?
当抛出异常时,你真正知道的是程序处于无效状态。你不应该对一些不可能的事情最终证明是可能的感到惊讶。就我个人而言,我见过更多 Alexandrescu 的建议最有意义的案例:尝试清理,但要认识到第一个异常意味着事情已经处于无效状态,因此会出现额外的故障——尤其是由第一个问题 ("error cascade")——不足为奇。试图处理它们不会有好结果。
我应该提一下,Cap'n Proto 完全按照您的建议进行:
When Cap’n Proto code might throw an exception from a destructor, it first checks
std::uncaught_exception()
to ensure that this is safe. If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel.
但是,正如 Yakk 所说,在 C++11 中,析构函数默认变为 nothrow(true)
。这意味着如果你想这样做,你需要确保在 C++11 和更高版本中你将析构函数标记为 nothrow(false)
。否则,即使没有其他异常在运行中,从析构函数中抛出异常也会终止程序。请注意,"If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel."