RAII什么时候比GC有优势?
考虑这个在 C++ 中演示 RAII 的简单类(从我的头脑中):
class X {
public:
X() {
fp = fopen("whatever", "r");
if (fp == NULL)
throw some_exception();
}
~X() {
if (fclose(fp) != 0){
// An error. Now what?
}
}
private:
FILE *fp;
X(X const&) = delete;
X(X&&) = delete;
X& operator=(X const&) = delete;
X& operator=(X&&) = delete;
}
我不能在析构函数中抛出异常。我遇到错误,但无法报告它。这个例子非常通用:我不仅可以使用文件来做到这一点,还可以使用例如 posix 线程、图形资源……我注意到维基百科 RAII 页面如何将整个问题掩盖起来: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
在我看来RAII 仅在保证销毁无错误地发生时才有用。据我所知,具有此属性的唯一资源是内存。现在在我看来,Boehm 相当令人信服地揭穿了手动内存管理在任何常见情况下都是一个好主意的想法,那么使用 RAII 的 C++ 方式的优势在哪里呢?
是的,我知道 GC 在 C++ 世界里有点异端;-)
Consider this simple class that demonstrates RAII in C++ (From the top of my head):
class X {
public:
X() {
fp = fopen("whatever", "r");
if (fp == NULL)
throw some_exception();
}
~X() {
if (fclose(fp) != 0){
// An error. Now what?
}
}
private:
FILE *fp;
X(X const&) = delete;
X(X&&) = delete;
X& operator=(X const&) = delete;
X& operator=(X&&) = delete;
}
I can't throw an exception in the destructor. I m having an error, but no way to report it. And this example is quite generic: I can do this not only with files, but also with e.g posix threads, graphical resources, ... I note how e.g. the wikipedia RAII page sweeps the whole issue under the rug: http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
It seems to me that RAII is only usefull if the destruction is guaranteed to happen without error. The only resources known to me with this property is memory. Now it seems to me that e.g. Boehm pretty convincingly debunks the idea of manual memory management is a good idea in any common situation, so where is the advantage in the C++ way of using RAII, ever?
Yes, I know GC is a bit heretic in the C++ world ;-)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
RAII 与 GC 不同,它是确定性的。您将确切地知道资源何时将被释放,而不是“将来的某个时候它将被释放”,具体取决于 GC 决定何时需要再次运行。
现在谈谈您似乎遇到的实际问题。这个讨论是在 Lounge中进行的。不久前的聊天室讨论了如果 RAII 对象的析构函数可能失败时应该做什么。
结论是,最好的方法是提供一个特定的
close()
、destroy()
或类似的成员函数,由析构函数调用,但也可以在析构函数之前调用如果您想避免“堆栈展开期间的异常”问题。然后它会设置一个标志来阻止它在析构函数中被调用。例如,std::(i|o)fstream 正是这样做的 - 它在析构函数中关闭文件,但还提供了 close() 方法。RAII, unlike GC, is deterministic. You will know exactly when a resource will be released, as opposed to "sometime in the future it's going to be released", depending on when the GC decides it needs to run again.
Now on to the actual problem you seem to have. This discussion came up in the Lounge<C++> chat room a while ago about what you should do if the destructor of a RAII object might fail.
The conclusion was that the best way would be provide a specific
close()
,destroy()
, or similar member function that gets called by the destructor but can also be called before that, if you want to circumvent the "exception during stack unwinding" problem. It would then set a flag that would stop it from being called in the destructor.std::(i|o)fstream
for example does exactly that - it closes the file in its destructor, but also provides aclose()
method.这是一个稻草人的论点,因为你不是在谈论垃圾收集(内存释放),而是在谈论一般资源管理。
如果您滥用垃圾收集器以这种方式关闭文件,那么您将遇到相同的情况:您也无法抛出异常。您也可以选择相同的选项:忽略错误,或者更好的是记录错误。
This is a straw man argument, because you're not talking about garbage collection (memory deallocation), you're talking about general resource management.
If you misused a garbage collector to close files this way, then you'd have the identical situation: you also could not throw an exception. The same options would be open to you: ignoring the error, or, much better, logging it.
垃圾收集中也会出现完全相同的问题。
但是,值得注意的是,如果您的代码或为您的代码提供支持的库代码中没有错误,则删除资源将永远失败。
delete
永远不会失败,除非你损坏了你的堆。对于每种资源来说都是同样的情况。未能销毁资源会导致应用程序终止崩溃,而不是令人愉快的“处理我”异常。The exact same problem occurs in garbage collection.
However, it's worth noting that if there is no bug in your code nor in the library code which powers your code, deletion of a resource shall never fail.
delete
never fails unless you corrupted your heap. This is the same story for every resource. Failure to destroy a resource is an application-terminating crash, not a pleasant "handle me" exception.析构函数中的异常从来没有用,原因很简单:析构函数会析构正在运行的代码不再需要的对象。释放期间发生的任何错误都可以通过上下文无关的方式安全地处理,例如记录、向用户显示、忽略或调用 std::terminate。周围的代码并不关心,因为它不再需要该对象。因此,您不需要通过堆栈传播异常并中止当前计算。
在您的示例中,
fp
可以安全地推入不可关闭文件的全局队列并稍后处理。调用代码可以毫无问题地继续。根据这个论点,析构函数很少需要抛出异常。在实践中,他们确实很少抛出异常,这解释了 RAII 的广泛使用。
Exceptions in destructors are never useful for one simple reason: Destructors destruct objects that the running code doesn't need anymore. Any error that happens during their deallocation can be safely handled in a context-agnostic way, like logging, displaying to the user, ignoring or calling
std::terminate
. The surrounding code doesn't care because it doesn't need the object anymore. Therefore, you don't need to propagate an exception through the stack and abort the current computation.In your example,
fp
could be safely pushed into a global queue of non-closeable files and handled later. The calling code can continue without problems.By this argument, destructors very rarely have to throw. In practice, they really do rarely throw, explaining the widespread use of RAII.
首先:如果您的文件对象被GCed,并且无法关闭FILE*,那么您实际上无法对错误执行任何有用的操作。因此,就这一点而言,两者是等效的。
其次,“正确”的模式如下:
用法:
如果“do stuff”抛出异常,那么文件句柄是否成功关闭几乎无关紧要。操作失败,因此该文件通常是无用的。调用链上层的人可能知道该怎么做,具体取决于文件的使用方式 - 也许应该删除它,也许应该将其保留在部分写入的状态。无论他们做什么,他们都必须意识到,除了他们看到的异常所描述的错误之外,文件缓冲区也可能没有被刷新。
RAII 在这里用于管理资源。无论如何,文件都会关闭。但 RAII 不用于检测操作是否成功 - 如果您想这样做,那么您可以调用
x.close()
。 GC 也不用于检测操作是否成功,因此两者在该计数上是相等的。每当您在定义某种事务的上下文中使用 RAII 时,您都会遇到类似的情况 - RAII 可以在异常时回滚打开的事务,但假设一切顺利,程序员必须显式提交该事务。
你的问题的答案 - RAII 的优点,以及你最终在 Java 中的
finally
子句中刷新或关闭文件对象的原因是,有时你希望清理资源(到目前为止尽可能)在退出作用域时立即执行,以便下一段代码知道它已经发生了。标记-清除 GC 不能保证这一点。First: you can't really do anything useful with the error if your file object is GCed, and fails to close the FILE*. So the two are equivalent as far as that goes.
Second, the "correct" pattern is as follows:
Usage:
If "do stuff" throws, then it pretty much doesn't matter whether the file handle is closed successfully or not. The operation has failed, so the file is normally useless anyway. Someone higher up the call chain might know what to do about that, depending how the file is used - perhaps it should be deleted, perhaps left alone in its partially-written state. Whatever they do, they must be aware that in addition to the error described by the exception they see, it's possible that the file buffer wasn't flushed.
RAII is used here for managing resources. The file gets closed no matter what. But RAII is not used for detecting whether an operation has succeeded - if you want to do that then you call
x.close()
. GC is also not used for detecting whether an operation has succeeded, so the two are equal on that count.You get a similar situation whenever you use RAII in a context where you're defining some kind of transaction -- RAII can roll back an open transaction on an exception, but assuming all goes OK, the programmer must explicitly commit the transaction.
The answer to your question -- the advantage of RAII, and the reason you end up flushing or closing file objects in
finally
clauses in Java, is that sometimes you want the resource to be cleaned up (as far as it can be) immediately on exit from the scope, so that the next bit of code knows that it has already happened. Mark-sweep GC doesn't guarantee that.我想补充一些有关“RAII”与 GC 的更多想法。使用某种关闭、销毁、完成等功能的方面已经被解释为确定性资源释放的方面。至少有两个更重要的工具可以通过使用析构函数来启用,从而以程序员控制的方式跟踪资源:
尽管这些用途不一定直接使用 RAII 技术,但它们是通过对内存进行更明确的控制来实现的。也就是说,垃圾收集在内存使用方面也具有明显的优势,例如在多个线程之间传递对象时。在理想的情况下,这两种技术都可用,并且 C++ 正在采取一些步骤来支持垃圾收集(有时称为“垃圾收集”,以强调它试图提供系统的无限内存视图,即收集的对象不是被破坏,但它们的内存位置被重用)。到目前为止的讨论并未遵循 C++/CLI 使用两种不同类型的引用和指针的路线。
I want to chip in a few more thoughts relating to "RAII" vs. GC. The aspects of using some sort of a close, destroy, finish, whatever function are already explained as is the aspect of deterministic resource release. There are, at least, two more important facilities which are enabled by using destructors and, thus, keeping track of resources in a programmer controlled fashion:
new
anddelete
it is possible to allocate memory e.g. from a pool and arrange for an even compacter memory use of objects known to be related. This can also be used to place objects into dedicated memory areas, e.g. shared memory or other address ranges for special hardware.Although these uses don't necessarily use RAII techniques directly, they are enabled by the more explicit control over memory. That said, there are also memory uses where garbage collection has a clear advantage e.g. when passing objects between multiple threads. In an ideal world both techniques would be available and C++ is taking some steps to support garbage collection (sometimes referred to as "litter collection" to emphasize that it is trying to give an infinite memory view of the system, i.e. collected objects aren't destroyed but their memory location is reused). The discussions so far don't follow the route taken by C++/CLI of using two different kinds of references and pointers.
问:RAII 何时比 GC 有优势?
答:在所有破坏错误不感兴趣的情况下(即您无论如何都没有有效的方法来处理这些错误)。
请注意,即使使用垃圾收集,您也必须手动运行“处置”(关闭、释放任何内容)操作,因此您可以以同样的方式改进 RIIA 模式:
Q. When has RAII an advantage over GC?
A. In all the cases where destruction errors are not interesting (i.e. you don't have an effective way to handle those anyway).
Note that even with garbage collection, you'd have to run the 'dispose' (close,release whatever) action manually, so you can just improve the RIIA pattern in the very same way:
析构函数被认为总是成功的。为什么不只是确保
fclose
不会失败呢?您始终可以手动执行
fflush
或其他一些操作并检查错误以确保fclose
稍后会成功。Destructors are assumed to be always success. Why not just make sure that
fclose
won't fail?You can always do
fflush
or some other things manually and check error to make sure thatfclose
will succeed later.