什么时候RAII比GC有优势? 考虑这个简单类,它演示C++中的RAII(从我的头上):

什么时候RAII比GC有优势? 考虑这个简单类,它演示C++中的RAII(从我的头上):,c++,raii,C++,Raii,我不能在析构函数中抛出异常。我有个错误,但没办法报告。这个例子非常通用:我不仅可以使用文件,还可以使用posix线程、图形资源等。。。我注意到wikipedia RAII页面是如何将整个问题隐藏起来的: 在我看来,RAII只有在确保销毁不会出错的情况下才有用。我知道的唯一具有此属性的资源是内存。在我看来,例如BeHM相当令人信服地驳斥了手动内存管理的想法在任何普通情况下都是一个好主意,那么C++在使用RAII的方式中的优势在哪里?p> 是的,我知道GC在C++世界中有点异端;p> 这是一个稻草人

我不能在析构函数中抛出异常。我有个错误,但没办法报告。这个例子非常通用:我不仅可以使用文件,还可以使用posix线程、图形资源等。。。我注意到wikipedia RAII页面是如何将整个问题隐藏起来的:

在我看来,RAII只有在确保销毁不会出错的情况下才有用。我知道的唯一具有此属性的资源是内存。在我看来,例如BeHM相当令人信服地驳斥了手动内存管理的想法在任何普通情况下都是一个好主意,那么C++在使用RAII的方式中的优势在哪里?p>
<>是的,我知道GC在C++世界中有点异端;p> 这是一个稻草人式的论点,因为你说的不是垃圾收集(内存释放),而是一般的资源管理


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

垃圾收集中也会出现同样的问题

但是,值得注意的是,如果您的代码中没有bug,也没有为代码提供支持的库代码中没有bug,那么删除资源将永远不会失败<代码>删除永远不会失败,除非您损坏了堆。每种资源的情况都是一样的。破坏资源失败是应用程序终止崩溃,而不是令人愉快的“handle me”异常。

RAII与GC不同,是确定性的。您将确切地知道资源将在何时释放,而不是“将来某个时候它将被释放”,这取决于GC何时决定它需要再次运行

现在来谈谈你似乎遇到的实际问题。不久前,在休息室聊天室中讨论了如果RAII对象的析构函数可能失败,应该怎么做


结论是,最好的方法是提供一个特定的
close()
destroy()
,或类似的成员函数,该函数由析构函数调用,但也可以在此之前调用,如果您想绕过“堆栈展开期间的异常”问题。然后,它将设置一个标志,阻止在析构函数中调用它<代码>标准::(i | o)fstream就是这样做的——它在析构函数中关闭文件,但也提供了一个
close()
方法。

Q.RAII什么时候比GC有优势

A.在所有销毁错误不感兴趣的情况下(即,您没有有效的方法来处理这些错误)

请注意,即使使用垃圾收集,您也必须手动运行“dispose”(关闭、释放任何内容)操作,因此您可以用同样的方法改进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
            }
        }
    }
}

第一:如果您的文件对象是GCed,并且无法关闭文件*,那么您将无法对该错误做任何有用的处理。因此,就这一点而言,两者是等价的

第二,“正确”的模式如下:

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
如果“DoStuff”抛出,那么文件句柄是否成功关闭几乎无关紧要。操作失败,因此该文件通常没有任何用处。调用链的上层人员可能知道该怎么做,这取决于文件的使用方式——也许应该将其删除,或者让其处于部分写入状态。无论他们做什么,他们必须意识到,除了他们看到的异常所描述的错误之外,文件缓冲区可能没有刷新

RAII在这里用于管理资源。不管发生什么,文件都会关闭。但是RAII不用于检测操作是否成功-如果要这样做,请调用
x.close()
。GC也不用于检测操作是否成功,因此两者在该计数上相等

当您在定义某种事务的上下文中使用RAII时,您会遇到类似的情况——RAII可以在异常时回滚打开的事务,但假设一切正常,程序员必须显式提交该事务


您的问题的答案——RAII的优势,以及您最终在Java中刷新或关闭
finally
子句中的文件对象的原因,是因为有时您希望在退出作用域时立即清理(尽可能地清理)资源,以便下一位代码知道它已经发生了。Mark sweep GC不能保证。析构函数中的异常永远不会有用,原因很简单:析构函数会析构函数运行的代码不再需要的对象。在解除分配期间发生的任何错误都可以通过上下文无关的方式安全地处理,如记录、向用户显示、忽略或调用
std::terminate
。周围的代码不在乎,因为它不再需要对象了。因此,不需要通过堆栈传播异常并中止当前计算

在您的示例中,
fp
可以安全地推送到不可关闭文件的全局队列中,稍后再进行处理。调用代码可以毫无问题地继续


根据这个论点,析构函数很少需要抛出。实际上,他们很少扔垃圾,这解释了RAII的广泛使用。

我想再谈谈关于“RAII”和GC的一些想法。使用某种关闭、销毁、完成等功能的各个方面已经被解释为确定性资源释放的方面。至少还有两个更重要的设施,它们通过使用析构函数启用,从而以程序员控制的方式跟踪资源:

  • 在RAII世界中,可能有一个过时的指针,即pointe
    X x;
    // do stuff involving x that might throw
    x.close(); // also might throw, but if not then the file is successfully closed