C++ 与 Java 和 C# 中的 Dispose 模式

Dispose pattern in C++ vs Java and C#

我有一些 Java 的背景知识(最近在 C# 方面),我也想更好地了解 C++。我想我知道这些语言之间内存(和其他资源)管理差异的一些基础知识。这可能是一个与使用 dispose pattern and the different features available in these languages to assist with it. I like what I've gathered of the RAII and SBRM 原则相关的小问题,我正在尝试进一步理解它们。

假设我在 Java

中有以下 class 和方法
class Resource implements Closeable {
    public void close() throws IOException {
        //deal with any unmanaged resources
    }
}
...
void useSomeResources() {
    try(Resource resource = new Resource()) {
        //use the resource
    }
    //do other things. Resource should have been cleaned up.
}

或相当接近的 C# 类似物

class Resource : IDisposable
{
    public void Dispose()
    {
        //deal with any unmanaged resources
    }
}
...
void UseSomeResources()
{
    using(var resource = new Resource())
    {
        //use the resource
    }
    //do other things. Resource should have been cleaned up.
}

我是否认为在 C++ 中最能代表这种相同行为的成语如下?

class Resource {
    ~Resource() {
        cleanup();
    }
    public:
    void cleanup() {
        //deal with any non-memory resources
    }
};
...
void useSomeResources()
{
    {
        Resource resource;
        //use the resource
    }
    //do other things. Stack allocated resource
    //should have been cleaned up by stack unwinding
    //on leaving the inner scope.
}

我特别不想引发关于谁的语言更好之类的争论,但我想知道这些实现可以在多大程度上进行比较,以及它们在阻止使用资源遇到异常情况。我可能完全错过了某些事情的要点,而且我对处置的最佳实践从来都不太确定——为了争论,也许值得假设这里的所有 disposal/destruction 函数都是幂等的——而且对于那些人来说,这是非常好的提示matters 也可能与这个问题有关。

感谢您的指点。

你已经提到了答案,就是RAII,就像你的link一样。

C++ 中典型的 class 将有一个(虚拟的!你忘记了)析构函数:

  class C { 
    virtual ~C { /*cleanup*/ }
  };

并且您使用正常的块规则控制它的生命周期:

  void f() {
    C c;

    // stuff

    // before this exits, c will be destructed
  }

这实际上是像 C# 和 Java 这样的语言试图用它们的处置模式来模拟的。由于它们没有确定性终结器,因此您必须手动释放非托管资源(分别使用 usingtry)。然而,C++ 是完全确定性的,所以这样做要简单得多。

差不多就这样了。事实上,您不需要添加 cleanup() 函数:析构函数在那里进行清理。

顺便说一句,暴露 public cleanup() 允许意外调用 cleanup(),使资源处于不希望的状态。

class Resource {
    ~Resource() {
        //deal with any non-memory resources
    }
};   // allways ; at the end of a class ;-)

(1)提议class,

class Resource {
    ~Resource() {
        cleanup();
    }
    public:
    void cleanup() {
        //deal with any non-memory resources
    }
};

是非惯用的和危险的,因为 (1) 它公开了 cleanup 操作,并且 (2) 它阻止了从中派生 classes,并阻止了这个 [=42] 的自动变量=].

暴露的 cleanup 可以在任何时候被任何代码调用,清理后你有不可用的 僵尸对象 。而且您不知道何时或是否会发生这种情况,因此实现代码必须在每个地方检查该状态。很不好。它与 init 具有构造函数角色的函数相当,只有一个虚拟构造函数。

类 实际上无法派生,因为在对象被销毁的派生 class 中,会生成对此 class 的析构函数的调用,并且该析构函数不可访问 –所以代码无法编译。

正确的模式如下所示:

class Resource
{
public:
    // Whatever, then:

    ~Resource()
    {
        // Clean up.
    }
};

仍然可以显式调用析构函数,但强烈建议不要这样做。

注意class推导和多态使用,析构函数最好做成virtual。但在其他情况下,这会不必要地使 class 多态,从而产生大小成本。所以这是一个工程决定。


(1) 我添加了一个缺少的分号。 post 真实代码是个好主意,即使对于一般的小例子也是如此。

感谢您的指点。哈!

你指的是 Java try with resources 方法,这是实际调用 resource.close() 的快捷方式。另一种选择是调用 resource.Dispose()

要记住的重要一点是,您在 Java 和 C# 中使用的 object 来关闭使用这些接口的东西需要 object 和 member-field 闭包.文件一旦打开就必须关闭。没有办法绕过它,并且试图摆脱它,会让你记忆犹新,并且会让其他应用程序因无法访问你声称拥有但从未访问过的文件而面临失败的风险关闭。重要的是您提供代码来关闭文件。

但是在object离开记忆的时候,还有一些其他的东西必须要摆脱掉。当那些 object 离开作用域时,那些事情就会发生。那就是调用 C++ 中的析构函数时,您在上面引用的内容。

Closeable 和 IDisposable 属于我所说的“Responsible”class。当它从作用域中删除 object 并释放您可用的指针的顶级内存时,它们超越了 class 的任何普通“析构函数”。他们还负责处理您可能没有考虑过的繁琐事情,或者以后可能会使系统面临风险的事情。这就像做一个父亲和做一个好父亲。父亲必须为他的孩子提供庇护,但一个好父亲知道什么对孩子最好,即使孩子或其他看护人不知道什么对他们最好。

请注意,当您想使用 Java 的“尝试使用资源”替代方案时,重要的是引用 AutoCloseable 接口而不是 Closeable 接口。

答案:IDisposableCloseable接口,甚至AutoCloseable接口,都支持移除托管资源。 C++ 中的“析构函数”也是如此,它是 shorthand 用于此类删除过程的祖父。问题是您仍然必须确保正确处理正在销毁的 class 成员。我认为你有正确的功能来调用 C++ 来做你想做的事情。

参考资料: http://www.completecsharptutorial.com/basic/using.php http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

总结一下这个问题的其他答案中提出的一些好的观点以及我读过的其他一些东西:

  • 主要问题的答案是肯定的,无论控制如何离开定义它的块,automatic local variable resource 都会调用其析构函数。在这方面,变量(而不是使用 new)的内部作用域和局部分配(通常意味着堆栈而不是堆,但取决于编译器)非常像 [=51= 中的 try-with-resources 块],或 C# 中的 using 块。
  • 与 Java 和 C# 相比,在 C++ 中纯粹在本地(通常意味着:到堆栈)分配对象的能力意味着,对于处理需要安全处理的资源的对象,额外的接口实现并且有些过度曝光 public 处理方法是不需要的(通常也是不可取的)。
    • 使用 private 析构函数,~Resource(),消除了意外使对象处于意外状态的一些危险(例如没有文件句柄的文件编写器),但 'unmanaged resources' 仍然总是当对象被删除时安全处置(或者超出范围,如果它是问题示例中的自动局部变量。)
    • 如果需要,使用 public 清除函数成员仍然是绝对可能的,但这通常是不必要的危险。如果必须创建清理成员 public,最好是析构函数本身,因为这对任何用户来说都是一个明显的 'self-documenting' 指示,它只应在非常罕见的情况下调用:最好只是使用 delete 或让本地分配的对象超出范围,让编译器完成调用析构函数的工作。它还消除了非析构函数 public 方法可能导致的任何混淆 ('should I call cleanup() before I delete this object or not?').
    • 如果要继承您的资源对象,重要的是要确保其析构函数既 virtual(可重写)又(至少与 protected 一样可见,以确保子类可以妥善处理。
  • 此外,通过直接在析构函数中实现清理,以及在离开自动变量范围后立即进行无垃圾收集器的破坏语义(以及在 deletedynamically-allocated variables 时),它变成了属性 和类型本身的责任 必须妥善处理,而不是简单地能够安全处理。

更地道的 C++ 用法示例:

class Resource {
    //whatever members are necessary
    public:
    //resource acquisition for which this object is responsible should be constrained to constructor(s)
    ~Resource() { //or virtual ~Resource() if inheritance desired
        //deal with resource cleanup
    }
};

按照问题中的建议使用时,此方法应确保安全地处理资源而不会发生泄漏。