我们在C ++中使用RAII的次数越多,我们就越发现自己的析构函数会进行非平凡的重新分配。现在,解除分配(终结,但是你想要调用它)可能会失败,在这种情况下,异常实际上是让楼上的任何人知道我们的释放问题的唯一方法。但是再说一次,抛出析构函数是一个坏主意,因为在堆栈展开期间可能会抛出异常。 std::uncaught_exception()
让你知道什么时候发生,但不是更多,所以除了让你在终止之前记录一条消息之外你没有太多可以做的,除非你愿意让你的程序处于未定义状态,其中一些东西被解除分配/最终确定,有些则没有。
一种方法是使用无抛出析构函数。但在许多情况下,这只是隐藏了一个真正的错误。例如,我们的析构函数可能会因为抛出某些异常而关闭一些RAII管理的数据库连接,并且这些数据库连接可能无法关闭。这并不一定意味着我们可以在此时终止程序。另一方面,记录和跟踪这些错误并不是每个案例的真正解决方案;否则我们就不需要开始例外了。 使用无抛出析构函数,我们还发现自己必须创建应该在销毁之前调用的“reset()”函数 - 但这只会破坏RAII的整个目的。
另一种方法只是let the program terminate,因为这是你可以做的最可预测的事情。
有些人建议链接异常,以便一次可以处理多个错误。但老实说,我从来没有真正看到用C ++完成的工作,我也不知道如何实现这样的东西。
所以它是RAII或例外。不是吗?我倾向于无抛出的破坏者;主要是因为它保持简单(r)。但我真的希望有一个更好的解决方案,因为正如我所说,我们使用RAII的次数越多,我们发现自己越多地使用执行非平凡事情的dtors。
附录
我正在添加链接到我发现的有趣的主题文章和讨论:
答案 0 :(得分:18)
你不应该从析构函数中抛出异常。
注意:已更新为重新评估标准中的更改:
在C ++中03 如果异常已经传播,则应用程序将终止。
在C ++中11
如果析构函数是noexcept
(默认值),则应用程序将终止。
以下内容基于C ++ 11
如果异常转义noexcept
函数,则如果堆栈甚至已解除,则它是实现定义。
以下内容基于C ++ 03
终止我的意思是立即停止。堆栈展开停止。不再需要析构函数。所有不好的东西。请参阅此处的讨论。
throwing exceptions out of a destructor
我不遵循(如不同意)你的逻辑,这会导致析构函数变得更复杂。
通过正确使用智能指针,这实际上使析构函数更简单,因为现在所有内容都变为自动化。每个班级都会将自己的小部分拼凑出来。这里没有脑部手术或火箭科学。 RAII的另一大胜利。
关于std :: uncaught_exception()的可能性,我指的是Herb Sutters article about why it does not work
答案 1 :(得分:8)
从最初的问题:
现在,解除分配(完成, 但是你想叫它)可能会失败, 在这种情况下,例外是真的 让任何人上楼的唯一方法 知道我们的释放问题
无法清理资源会显示:
程序员错误,在这种情况下,您应记录失败,然后根据应用程序方案通知用户或终止应用程序。例如,释放已经释放的分配。
分配器错误或设计缺陷。请参阅文档。有可能是错误可能有助于诊断程序员错误。见上文第1项。
否则无法恢复的不利条件可以继续。
例如,C ++免费存储区有一个无失败的操作符删除。其他API(如Win32)提供错误代码,但只会因程序员错误或硬件故障而失败,错误指示堆损坏或双重释放等情况。
对于不可恢复的不利条件,请使用DB连接。如果关闭连接失败,因为连接被删除 - 很酷,你就完成了。不要扔!断开的连接(应该)导致关闭连接,因此不需要做任何其他事情。如果有,请记录跟踪消息以帮助诊断使用问题。例如:
class DBCon{
public:
DBCon() {
handle = fooOpenDBConnection();
}
~DBCon() {
int err = fooCloseDBConnection();
if(err){
if(err == E_fooConnectionDropped){
// do nothing. must have timed out
} else if(fooIsCriticalError(err)){
// critical errors aren't recoverable. log, save
// restart information, and die
std::clog << "critical DB error: " << err << "\n";
save_recovery_information();
std::terminate();
} else {
// log, in case we need to gather this info in the future,
// but continue normally.
std::clog << "non-critical DB error: " << err << "\n";
}
}
// done!
}
};
这些条件都没有理由尝试第二种放松。程序可以正常继续(包括异常展开,如果正在进行展开),或者它现在就会消失。
修改-添加
如果确实希望能够保持与那些无法关闭的数据库连接的某种链接 - 也许它们由于间歇性条件而无法关闭,并且您希望稍后重试 - 然后你总是可以推迟清理:
vector<DBHandle> to_be_closed_later; // startup reserves space
DBCon::~DBCon(){
int err = fooCloseDBConnection();
if(err){
..
else if( fooIsRetryableError(err) ){
try{
to_be_closed.push_back(handle);
} catch (const bad_alloc&){
std::clog << "could not close connection, err " << err << "\n"
}
}
}
}
非常不漂亮,但可能会为你完成工作。
答案 2 :(得分:4)
答案 3 :(得分:3)
你正在看两件事:
RAII承诺它将完成操作(空闲内存,关闭尝试刷新它的文件,结束尝试提交它的事务)。但是因为它是自动发生的,没有程序员必须做任何事情,它不会告诉程序员它“尝试”的操作是否成功。
异常是报告某些内容失败的一种方法,但正如您所说,C ++语言的局限性意味着它们不适合从析构函数[*]执行此操作。返回值是另一种方式,但更明显的是析构函数也不能使用它们。
因此,如果您想知道您的数据是否已写入磁盘,则无法使用RAII。它并没有“打败RAII的整个目的”,因为RAII仍然会尝试编写它,它仍然会释放与文件句柄相关的资源(数据库事务,无论如何)。它确实限制了RAII可以做什么 - 它不会告诉你数据是否写入,所以你需要一个close()
函数来返回一个值和/或抛出异常。
[*]这也是一个很自然的限制,存在于其他语言中。如果您认为RAII析构函数应该抛出异常来说“出现问题!”,那么某些事情必须在飞行中已经出现异常时发生,即“在此之前出现了其他问题! ”。我知道使用异常的语言不允许同时出现两个例外 - 语言和语法根本不允许这样做。如果RAII要做你想做的事情,那么异常本身需要重新定义,这样一个线程一次就会出现多个错误,并且有两个异常向外传播,两个处理程序被调用,一个人来处理每一个。
其他语言允许第二个异常模糊第一个异常,例如,如果finally
块抛出Java。 C ++几乎说必须抑制第二个,否则调用terminate
(在某种意义上抑制两者)。在这两种情况下都没有更高的堆栈级别通知两个故障。有点不幸的是,在C ++中你无法可靠地判断是否有一个例外太多(uncaught_exception
没有告诉你,它告诉你一些不同的东西),所以你甚至无法抛出如果不是已经是飞行中的例外。但即使你可以在这种情况下做到这一点,你仍然会被填充在另外一个太多的情况下。
答案 4 :(得分:2)
我要问的一件事是,忽略终止等问题,如果您的程序无法关闭其数据库连接,或者由于正常的破坏或异常破坏,您认为适当的响应是什么。
您似乎排除“仅仅记录”并且不愿意终止,那么您认为最好的做法是什么?
我认为如果我们对这个问题有了答案,那么我们就会更好地了解如何继续。
对我来说,没有任何策略显得特别明显;除了其他任何东西,我真的不知道关闭数据库连接是什么意思。如果close()抛出,连接的状态是什么?它是封闭的,仍然是开放的还是不确定的?如果它不确定,程序是否有任何方法可以恢复到已知状态?
析构函数失败意味着无法撤消对象的创建;将程序恢复到已知(安全)状态的唯一方法是拆除整个过程并重新开始。
答案 5 :(得分:1)
您的销毁可能失败的原因是什么?为什么不在实际破坏之前处理那些?
例如,关闭数据库连接可能是因为:
如果我理解RAII(我可能不会),那么重点是它的范围。所以它不像你想要的交易持续时间长于对象。那么,对我来说,你想要尽可能确保关闭是合理的。 RAII没有使这个独特 - 即使没有任何对象(比如在C中),你仍然会尝试捕捉所有错误条件并尽可能地处理它们(有时候会忽略它们)。无论有多少函数使用该资源类型,所有RAII都会强制您将所有代码放在一个地方。
答案 6 :(得分:0)
您可以通过检查
判断当前是否存在异常(例如,我们介于执行堆栈展开的throw和catch块之间,可能正在复制异常对象或类似对象)bool std::uncaught_exception()
如果它返回true,则此时抛出将终止程序,如果不是,则抛出是安全的(或者至少与以前一样安全)。这将在ISO 14882(C ++标准)的第15.2节和第15.5.3节中讨论。
这不能回答在清理异常时遇到错误时该怎么做的问题,但实际上没有任何好的答案。但是如果你在后一种情况下等待做一些不同的事情(比如记录并忽略它),它确实可以让你区分正常退出和异常退出,而不是简单地慌张。