RAII何时优于GC?

时间:2012-01-03 12:58:46

标签: c++ raii

考虑这个用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相当令人信服地揭穿了手动内存管理的想法在任何常见情况下都是一个好主意,那么C ++使用RAII的方式的优势在哪里呢?

是的,我知道GC在C ++世界中有点异端; - )

8 个答案:

答案 0 :(得分:14)

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

现在谈谈你似乎遇到的实际问题。这个讨论出现在Lounge< C ++>聊天室前一段时间,如果RAII对象的析构函数可能失败,你应该怎么做。

结论是,最好的方法是提供一个特定的close()destroy()或类似的成员函数,它可以被析构函数调用,但如果你想要规避,也可以在之前调用它。 “堆栈展开期间的异常”问题。然后它会设置一个标志,以阻止它在析构函数中被调用。例如std::(i|o)fstream就是这样 - 它在析构函数中关闭文件,但也提供了close()方法。

答案 1 :(得分:13)

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

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

答案 2 :(得分:7)

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

但是,值得注意的是,如果代码中没有错误,也没有代码中的代码,那么删除资源永远不会失败。除非您损坏堆,否则delete永远不会失败。每个资源都是一样的故事。未能销毁资源是一个应用程序终止崩溃,而不是一个令人愉快的“处理我”例外。

答案 3 :(得分:4)

首先:如果文件对象是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

如果“do stuff”抛出,那么文件句柄是否成功关闭几乎无关紧要。操作失败,因此无论如何文件通常都是无用的。根据文件的使用方式,调用链上方的某个人可能知道应该怎么做 - 可能应该将其删除,也许在其部分写入的状态下保持不变。无论他们做什么,他们都必须意识到,除了他们看到的异常所描述的错误之外,还有可能没有刷新文件缓冲区。

RAII用于管理资源。无论如何,文件都会关闭。但是RAII不用于检测操作是否成功 - 如果你想这样做,那么你调用x.close()。 GC也不用于检测操作是否成功,因此两者在该计数上是相等的。

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

您的问题的答案 - RAII的优势,以及您最终在Java中finally子句中刷新或关闭文件对象的原因是,有时您希望清理资源(目前为止)因为它可以)立即从范围退出,以便下一位代码知道它已经发生。 Mark-sweep GC不保证。

答案 4 :(得分:4)

析构函数中的异常从来没有用过一个简单的原因:析构函数会破坏正在运行的代码不再需要的对象。在解除分配期间发生的任何错误都可以以与上下文无关的方式安全地处理,例如记录,向用户显示,忽略或调用std::terminate。周围的代码并不关心,因为它不再需要该对象。因此,您不需要通过堆栈传播异常并中止当前计算。

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

通过这个论点,析构函数很少需要抛出。在实践中,他们确实很少抛出,解释了RAII的广泛使用。

答案 5 :(得分:4)

我想更多关于“RAII”与GC相关的想法。使用某种关闭,破坏,完成,任何功能的方面已被解释为确定性资源释放的方面。至少有两个更重要的设施可以通过使用析构函数来实现,从而以程序员控制的方式跟踪资源:

  1. 在RAII世界中,可能有一个陈旧的指针,即一个指向已经被破坏的对象的指针。听起来像Bad Thing的东西实际上使相关对象能够在内存中非常接近。即使它们不适合相同的缓存行,它们至少也会适合内存页面。在某种程度上,通过压缩垃圾收集器可以实现更接近的接近,但在C ++世界中,这是自然而然的,并且已经在编译时确定。
  2. 虽然通常只使用运算符newdelete分配和释放内存,但可以分配内存,例如从一个游泳池,并安排一个更加平坦的记忆使用已知相关的物体。这也可以用于将对象放置在专用存储区域中,例如,共享内存或特殊硬件的其他地址范围。
  3. 虽然这些用途不一定直接使用RAII技术,但它们可以通过更明确的内存控制来实现。也就是说,还有内存使用,其中垃圾收集具有明显的优势,例如在多个线程之间传递对象时。在理想的世界中,这两种技术都是可用的,C ++正在采取一些步骤来支持垃圾收集(有时也称为“垃圾收集”,以强调它试图给出系统的无限内存视图,即收集的对象不是被破坏,但他们的记忆位置被重复使用)。到目前为止的讨论并不遵循C ++ / CLI使用两种不同类型的引用和指针所采用的路径。

答案 6 :(得分:2)

Q值。 RAII何时优于GC?

一个。在所有破坏错误都没有意义的情况下(即你无论如何都没有有效的方法来处理这些错误)。

请注意,即使使用垃圾收集,您也必须手动运行'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
            }
        }
    }
}

答案 7 :(得分:1)

假设析构函数总是成功的。为什么不确保fclose不会失败?

您可以随时手动执行fflush或其他操作并检查错误,以确保fclose稍后成功。