【问题标题】:Dispose pattern in C++ vs Java and C# [closed]在 C++ 与 Java 和 C# 中处理模式 [关闭]
【发布时间】:2015-08-01 18:45:59
【问题描述】:

我有一些 Java 方面的背景(最近在 C# 方面),也想更好地了解 C++。我想我知道这些语言之间内存(和其他资源)管理差异的一些基础知识。这可能是一个与使用dispose pattern 以及这些语言中可用的不同功能来协助它相关的小问题。我喜欢我收集的 RAII and SBRM 原则,我正在努力进一步理解它们。

假设我在 Java 中有以下类和方法

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.
}

我特别不想引发关于谁的语言更好之类的争论,但我想知道这些实现可以在多大程度上进行比较,以及它们对于块使用的情况有多健壮资源遇到特殊情况。我可能完全错过了某些事情的重点,而且我永远不太确定处置的最佳实践——为了争论,也许值得假设这里的所有处置/销毁函数都是幂等的——而且这些问题的真正好技巧可能也与这个问题有关。

感谢您的任何指点。

【问题讨论】:

  • 是的,这是模式的等价物,因为 resource 将在其封闭范围的末尾被销毁。
  • 不,这不是模式的等价物。

标签: c++ resources raii


【解决方案1】:

这几乎是模式。事实上,您不需要添加cleanup() 函数:析构函数可以进行清理。

顺便说一句,公开cleanup() 允许意外调用cleanup(),从而使资源处于不希望的状态。

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

【讨论】:

  • 谢谢你——事实上我想知道那个方法的访问级别。我认为要并行处理也可以随时调用的 Dispose() 和 close() 方法,它将是公开的。我同意这是一种危险的方式,除非小心谨慎,这也是我非常喜欢我对 SBRM 的理解的部分原因!已添加分号。菜鸟错误。
  • P.s 这就是我提到清理的幂等性的部分原因,但当然仍然存在意外过早清理对象的问题。我想我会喜欢 C++ :)
  • 使用我写这篇文章时显示的类,不能声明自动变量。这使得它不太理想。
【解决方案2】:

这个(1)提议的类,

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

是非惯用且危险的,因为 (1) 它公开了 cleanup 操作,以及 (2) 它阻止了从中派生类,并阻止了此类的自动变量。

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

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

正确的模式如下所示:

class Resource
{
public:
    // Whatever, then:

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

析构函数仍然可以显式调用,但有强烈的动机不这样做。

请注意,对于类派生和多态使用,最好将析构函数设为virtual。但在其他情况下,这会不必要地使类多态,从而产生大小成本。所以这是一个工程决策。


(1) 我添加了一个缺少的分号。发布真实代码是个好主意,即使是小的通用示例也是如此。

【讨论】:

  • 感谢您指出缺少的分号。菜鸟错误!正如我对@Christophe 的回答一样,我很欣赏使它更惯用的技巧——似乎 IDisposable 和 Closeable 接口的存在是解决在这些语言中不可能进行本地对象的堆栈分配这一事实的解决方法。因此,将销毁方法设为私有在 C++ 中非常有意义。但是,如果显式调用析构函数,这实际上并不会删除对象,对吗?所以区别只是代码更好地自我记录,说“调用这个需要你自担风险”?
  • @Oly'Oil'Sourbut:关于“将销毁方法设为私有在 C++ 中很有意义”,您可能在这里谈论的是从析构函数调用的 cleanup 方法.但是,如果您不是,请注意,如前所述,使用 private 析构函数可以防止自动变量和(实际上是在实践中)类派生。 protected 析构函数只阻止自动变量,因此实际上只强制执行动态分配的一种方法(另一种曾经被推荐的方法是使构造函数非public 并提供工厂函数,这在很大程度上是 Java 和 C#方式)。
  • @Oly'Oil'Sourbut:重新“如果显式调用析构函数,这实际上并没有删除对象,但是,我是对的吗?”,从技术上讲你是对的:它没有不要进行重新分配。但它搞砸了类不变量,即其他方法所依赖的假设。析构函数通常只被自动调用,然后你可以保证每个被销毁的对象都调用一个且恰好一个析构函数。恰巧,Visual C++ 6.0 在 1990 年代后期有一个错误,它会调用异常对象的析构函数两次。这有点讨厌,但很高兴它只发生在做“高级”事情时。
【解决方案3】:

您已经提到了答案,它是 RAII,就像在您的链接中一样。

一个典型的 C++ 类会有一个(虚拟的!你忘了)析构函数:

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

你可以用普通的块规则控制它的生命周期:

  void f() {
    C c;

    // stuff

    // before this exits, c will be destructed
  }

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

【讨论】:

  • 虚拟析构函数不是必需的,在我的代码中当然不是典型的。
  • 如果不涉及继承,则将析构函数设为虚拟是没有意义的。
  • 请务必投反对票,因为您不使用它们!
  • 我(有点)知道如何在 C++ 中处理继承和虚方法,但我想将我给出的代码示例减少到最低限度。当涉及继承时,当然值得记住将析构函数设为虚拟;谢谢!
【解决方案4】:

感谢您的任何指点。哈!

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

要记住的重要一点是,您在 Java 和 C# 中使用这些接口来关闭事物的对象需要对象和成员字段关闭。文件一旦打开就必须关闭。没有办法解决它,并且试图摆脱它,会让你记忆犹新,并且会让其他应用程序面临失败的风险,因为无法访问你声称但从未访问过的文件关闭。提供关闭文件的代码很重要。

但是当对象离开内存时,还有一些其他的东西必须被删除。当这些对象离开范围时,这些事情就会发生。这就是你在上面引用的 C++ 中的析构函数被调用的时候。

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

请注意,当您想使用 Java 的“try with Resources”替代方案时,引用AutoCloseable 接口而不一定是Closeable 接口很重要。

答案:IDisposableCloseable 接口,甚至AutoCloseable 接口都支持删除托管资源。 C++ 中的“析构函数”也是如此,它是这种删除过程的简写。问题是您仍然必须确保正确处理正在遭受破坏的班级成员。我认为你有正确的功能来调用 C++ 来做你想做的事情。

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

【讨论】:

    【解决方案5】:

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

    • 主要问题的答案是肯定的,automatic local variableresource 的析构函数被调用,无论控制如何离开定义它的块。在这方面,变量(而不是使用new)的内部范围和本地分配(通常意味着堆栈而不是堆,但取决于编译器)的行为非常类似于Java 中的try-with-resources 块或using 块在 C# 中。
    • 与 Java 和 C# 相比,在 C++ 中纯粹在本地分配对象(通常意味着:到堆栈)的能力意味着,对于处理需要安全处置的资源的对象,额外的接口实现和有些过度暴露的公共处置方法不需要(而且通常是不可取的)。
      • 使用private 析构函数~Resource() 消除了一些意外使对象处于意外状态的危险(例如,没有文件句柄的文件写入器),但是当对象被删除时,“非托管资源”仍然总是安全地处置(或者如果它是问题示例中的自动局部变量,则超出范围。)
      • 如果需要,使用public 清理函数成员仍然是绝对可行的,但这通常是不必要的危险。如果清理成员必须公开,最好是析构函数本身,因为这对任何用户来说都是一个明显的“自我记录”指示,它应该只在非常罕见的情况下调用:最好只使用delete或者让本地分配的对象超出范围,让编译器完成调用析构函数的工作。它还消除了非析构函数公共方法可能导致的任何混淆('我应该在delete 这个对象之前调用cleanup() 吗?')。
      • 如果要继承您的资源对象,重要的是要确保它的析构函数既是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
        }
    };
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-03-01
      • 2020-12-30
      • 2012-08-28
      • 1970-01-01
      • 1970-01-01
      • 2021-10-23
      • 2010-12-10
      相关资源
      最近更新 更多