RAII什么时候比GC有优势?

发布于 2024-12-24 20:29:46 字数 882 浏览 3 评论 0原文

考虑这个在 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

猥︴琐丶欲为 2024-12-31 20:29:46

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 a close() method.

蹲在坟头点根烟 2024-12-31 20:29:46

这是一个稻草人的论点,因为你不是在谈论垃圾收集(内存释放),而是在谈论一般资源管理。

如果您滥用垃圾收集器以这种方式关闭文件,那么您将遇到相同的情况:您也无法抛出异常。您也可以选择相同的选项:忽略错误,或者更好的是记录错误。

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.

此岸叶落 2024-12-31 20:29:46

垃圾收集中也会出现完全相同的问题。

但是,值得注意的是,如果您的代码或为您的代码提供支持的库代码中没有错误,则删除资源将永远失败。 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.

流殇 2024-12-31 20:29:46

析构函数中的异常从来没有用,原因很简单:析构函数会析构正在运行的代码不再需要的对象。释放期间发生的任何错误都可以通过上下文无关的方式安全地处理,例如记录、向用户显示、忽略或调用 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.

尘曦 2024-12-31 20:29:46

首先:如果您的文件对象被GCed,并且无法关闭FILE*,那么您实际上无法对错误执行任何有用的操作。因此,就这一点而言,两者是等效的。

其次,“正确”的模式如下:

class X{
    FILE *fp;
  public:
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }
    ~X(){
        try {
            close();
        } catch (const FileError &) {
            // perhaps log, or do nothing
        }
    }
    void close() {
        if (fp != 0) {
            if(fclose(fp)!=0){
               // may need to handle EAGAIN and EINTR, otherwise
               throw FileError();
            }
            fp = 0;
        }
    }
};

用法:

X x;
// do stuff involving x that might throw
x.close(); // also might throw, but if not then the file is successfully closed

如果“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:

class X{
    FILE *fp;
  public:
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }
    ~X(){
        try {
            close();
        } catch (const FileError &) {
            // perhaps log, or do nothing
        }
    }
    void close() {
        if (fp != 0) {
            if(fclose(fp)!=0){
               // may need to handle EAGAIN and EINTR, otherwise
               throw FileError();
            }
            fp = 0;
        }
    }
};

Usage:

X x;
// do stuff involving x that might throw
x.close(); // also might throw, but if not then the file is successfully closed

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.

小兔几 2024-12-31 20:29:46

我想补充一些有关“RAII”与 GC 的更多想法。使用某种关闭、销毁、完成等功能的方面已经被解释为确定性资源释放的方面。至少有两个更重要的工具可以通过使用析构函数来启用,从而以程序员控制的方式跟踪资源:

  1. 在 RAII 世界中,可能有一个过时的指针,即指向一个对象的指针。已经销毁的对象。听起来像是一件坏事,实际上使相关对象能够在内存中紧密相连。即使它们不适合同一缓存行,它们至少也会适合内存页。在某种程度上,也可以通过压缩垃圾收集器来实现更接近,但在 C++ 世界中,这是自然而然的,并且已经在编译时确定了。
  2. 虽然通常只是使用运算符 new 和 delete 来分配和释放内存,但可以从池中分配内存,并安排已知对象的更紧凑的内存使用。有关的。这也可以用于将对象放入专用内存区域,例如共享内存或特殊硬件的其他地址范围。

尽管这些用途不一定直接使用 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:

  1. In the RAII world it is possible to have a stale pointer, i.e. a pointer which points to an already destroyed object. What sounds like a Bad Thing actually enables related objects to be located in close proximity in memory. Even if they don't fit onto the same cache-line they would, at least, fit into the memory page. To some extend closer proximity could be achieved by a compacting garbage collector, as well, but in the C++ world this comes naturally and is determined already at compile-time.
  2. Although typically memory is just allocated and released using operators new and delete 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.

不喜欢何必死缠烂打 2024-12-31 20:29:46

问:RAII 何时比 GC 有优势?

答:在所有破坏错误不感兴趣的情况下(即您无论如何都没有有效的方法来处理这些错误)。

请注意,即使使用垃圾收集,您也必须手动运行“处置”(关闭、释放任何内容)操作,因此您可以以同样的方式改进 RIIA 模式:

class X{
    FILE *fp;
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }

    void close()
    {
        if (!fp)
            return;
        if(fclose(fp)!=0){
            throw some_exception();
        }
        fp = 0;
    }

    ~X(){
        if (fp)
        {
            if(fclose(fp)!=0){
                //An error. You're screwed, just throw or std::terminate
            }
        }
    }
}

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:

class X{
    FILE *fp;
    X(){
      fp=fopen("whatever","r");
      if(fp==NULL) throw some_exception();
    }

    void close()
    {
        if (!fp)
            return;
        if(fclose(fp)!=0){
            throw some_exception();
        }
        fp = 0;
    }

    ~X(){
        if (fp)
        {
            if(fclose(fp)!=0){
                //An error. You're screwed, just throw or std::terminate
            }
        }
    }
}
べ映画 2024-12-31 20:29:46

析构函数被认为总是成功的。为什么不只是确保 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 that fclose will succeed later.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文