C++/CLI 中重复的析构函数调用和跟踪句柄 [英] Repeated destructor calls and tracking handles in C++/CLI

查看:40
本文介绍了C++/CLI 中重复的析构函数调用和跟踪句柄的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用 C++/CLI,使用 MSDN 文档和 ECMA 标准 和 Visual C++ Express 2010.让我印象深刻的是以下与 C++ 的不同:

I'm playing around with C++/CLI, using the MSDN documentation and the ECMA standard, and Visual C++ Express 2010. What struck me was the following departure from C++:

对于 ref 类,必须编写终结器和析构器,以便它们可以在尚未完全构造的对象上多次执行.

For ref classes, both the finalizer and destructor must be written so they can be executed multiple times and on objects that have not been fully constructed.

我编造了一个小例子:

#include <iostream>

ref struct Foo
{
    Foo()  { std::wcout << L"Foo()
"; }
    ~Foo() { std::wcout << L"~Foo()
"; this->!Foo(); }
    !Foo() { std::wcout << L"!Foo()
"; }
};

int main()
{
    Foo ^ r;

    {
        Foo x;
        r = %x;
    }              // #1

    delete r;      // #2
}

#1 块的末尾,自动变量 x死亡,析构函​​数被调用(反过来显式调用终结器,就像惯用语).这一切都很好.但是后来我通过引用 r 再次删除了该对象!输出是这样的:

At the end of the block at #1, the automatic variable xdies, and the destructor is called (which in turn calls the finalizer explicitly, as is the usual idiom). This is all fine and well. But then I delete the object again through the reference r! The output is this:

Foo()
~Foo()
!Foo()
~Foo()
!Foo()

问题:

  1. 在第 #2 行调用 delete r 是未定义的行为,还是完全可以接受?

  1. Is it undefined behavior, or is it entirely acceptable, to call delete r on line #2?

如果我们删除行 #2,那么 r 是否仍然是一个对象的跟踪句柄(在 C++ 的意义上)不再是存在吗?它是一个悬垂的把手"吗?它的引用计数是否会导致尝试双重删除?

If we remove line #2, does it matter that r is still a tracking handle for an object that (in the sense of C++) no longer exists? Is it a "dangling handle"? Does its reference counting entail that there will be an attempted double deletion?

我知道没有实际双重删除,因为输出变成了这样:

I know that there isn't an actual double deletion, as the output becomes this:

Foo()
~Foo()
!Foo()

但是,我不确定这是一次愉快的事故,还是可以保证是明确定义的行为.

However, I'm not sure whether that's a happy accident or guaranteed to be well-defined behaviour.

在哪些情况下可以多次调用托管对象的析构函数?

Under which other circumstances can the destructor of a managed object be called more than once?

r = %x; 之前或之后插入 x.~Foo(); 是否可以?

Would it be OK to insert x.~Foo(); immediately before or after r = %x;?

换句话说,托管对象是否永远存在"并且可以一遍又一遍地调用它们的析构函数和终结函数?

In other words, do managed objects "live forever" and can have both their destructors and their finalizers called over and over again?

响应@Hans 对非平凡类的需求,您也可以考虑这个版本(使用析构函数和终结器以符合多次调用要求):

In response to @Hans's demand for a non-trivial class, you may also consider this version (with destructor and finalizer made to conform to the multiple-call requirement):

ref struct Foo
{
    Foo()
    : p(new int[10])
    , a(gcnew cli::array<int>(10))
    {
        std::wcout << L"Foo()
";
    }

    ~Foo()
    {
        delete a;
        a = nullptr;

        std::wcout << L"~Foo()
";
        this->!Foo();
    }

    !Foo()
    {
        delete [] p;
        p = nullptr;

        std::wcout << L"!Foo()
";
    }

private:
    int             * p;
    cli::array<int> ^ a;
};

推荐答案

我将尝试按顺序解决您提出的问题:

I'll just try to address the issues you bring up in order:

对于 ref 类,必须编写终结器和析构器,以便它们可以在尚未完全构造的对象上多次执行.

For ref classes, both the finalizer and destructor must be written so they can be executed multiple times and on objects that have not been fully constructed.

析构函数 ~Foo() 简单地自动生成两个方法,一个是 IDisposable::Dispose() 方法的实现,一个是受保护的 Foo::Dispose(bool) 方法,它实现了一次性模式.这些是普通方法,因此可能会被多次调用.在 C++/CLI 中允许直接调用终结器,this->!Foo() 并且通常这样做,就像你做的那样.垃圾收集器只调用一次终结器,它在内部跟踪是否完成.鉴于允许直接调用终结器并且允许多次调用 Dispose(),因此可以多次运行终结器代码.这是特定于 C++/CLI 的,其他托管语言不允许这样做.您可以轻松阻止它,nullptr 检查通常可以完成工作.

The destructor ~Foo() simply auto-generates two methods, an implementation of the IDisposable::Dispose() method as well as a protected Foo::Dispose(bool) method which implements the disposable pattern. These are plain methods and therefore may be invoked multiple times. It is permitted in C++/CLI to call the finalizer directly, this->!Foo() and is commonly done, just like you did. The garbage collector only ever calls the finalizer once, it keeps track internally whether or not that was done. Given that calling the finalizer directly is permitted and that calling Dispose() multiple times is allowed, it is thus possible to run the finalizer code more than once. This is specific to C++/CLI, other managed languages don't allow it. You can easily prevent it, a nullptr check usually gets the job done.

在第 2 行调用 delete r 是未定义的行为,还是完全可以接受?

Is it undefined behavior, or is it entirely acceptable, to call delete r on line #2?

这不是 UB,完全可以接受.delete 运算符只需调用 IDisposable::Dispose() 方法并因此运行您的析构函数.你在其中所做的,通常是调用非托管类的析构函数,很可能会调用 UB.

It is not UB and entirely acceptable. The delete operator simply calls the IDisposable::Dispose() method and thus runs your destructor. What you do inside it, very typically calling the destructor of an unmanaged class, may well invoke UB.

如果我们删除第 2 行,r 仍然是一个跟踪句柄是否重要

If we remove line #2, does it matter that r is still a tracking handle

没有.调用析构函数完全是可选的,没有很好的方法来强制执行它.没有任何问题,终结器最终将始终运行.在给定的示例中,当 CLR 在关闭之前最后一次运行终结器线程时会发生这种情况.唯一的副作用是程序运行繁重",占用资源的时间超过了必要的时间.

No. Invoking the destructor is entirely optional without a good way to enforce it. Nothing goes wrong, the finalizer ultimately will always run. In the given example that will happen when the CLR runs the finalizer thread one last time before shutting down. The only side effect is that the program runs "heavy", holding on to resources longer than necessary.

在哪些情况下可以多次调用托管对象的析构函数?

Under which other circumstances can the destructor of a managed object be called more than once?

这很常见,过分热心的 C# 程序员可能会多次调用您的 Dispose() 方法.同时提供 Close 和 Dispose 方法的类在框架中非常常见.有一些模式几乎是不可避免的,例如另一个类假定一个对象的所有权.标准示例是这段 C# 代码:

It's pretty common, an overzealous C# programmer may well call your Dispose() method more than once. Classes that provide both a Close and a Dispose method are pretty common in the framework. There are some patterns where it is nearly unavoidable, the case where another class assumes ownership of an object. The standard example is this bit of C# code:

using (var fs = new FileStream(...))
using (var sw = new StreamWriter(fs)) {
    // Write file...
}

StreamWriter 对象将获得其基本流的所有权,并在最后一个大括号处调用其 Dispose() 方法.FileStream 对象上的 using 语句第二次调用 Dispose().编写此代码以使这种情况不会发生并且仍然提供异常保证太困难了.指定可以多次调用 Dispose() 可以解决问题.

The StreamWriter object will take ownership of its base stream and call its Dispose() method at the last curly brace. The using statement on FileStream object calls Dispose() a second time. Writing this code so that this doesn't happen and still provide exception guarantees is too difficult. Specifying that Dispose() may be called more than once solves the problem.

插入 x.~Foo(); 可以吗?紧接在 r = %x; 之前或之后?

Would it be OK to insert x.~Foo(); immediately before or after r = %x;?

没关系.结果不太可能令人愉快,NullReferenceException 将是最可能的结果.这是您应该测试的东西,引发 ObjectDisposedException 为程序员提供更好的诊断.所有标准 .NET 框架类都这样做.

It's okay. The outcome is unlikely to be pleasant, a NullReferenceException would be the most likely result. This is something that you should test for, raise an ObjectDisposedException to give the programmer a better diagnostic. All standard .NET framework classes do so.

换句话说,托管对象是否永远存在"

In other words, do managed objects "live forever"

不,当垃圾收集器无法再找到对该对象的任何引用时,它会声明该对象已死并收集它.这是一种安全的内存管理方式,没有办法意外引用已删除的对象.因为这样做需要一个引用,一个 GC 将始终看到的引用.循环引用等常见的内存管理问题也不是问题.

No, the garbage collector declares the object dead, and collects it, when it cannot find any references to the object anymore. This is a fail-safe way to memory management, there is no way to accidentally reference a deleted object. Because doing so requires a reference, one that the GC will always see. Common memory management problems like circular references are not an issue either.

代码片段

删除 a 对象是不必要的并且没有任何效果.您只删除实现 IDisposable 的对象,数组不会这样做.通用规则是 .NET 类仅在管理内存以外的资源时才实现 IDisposable.或者,如果它有一个本身实现 IDisposable 的类类型的字段.

Deleting the a object is unnecessary and has no effect. You only delete objects that implement IDisposable, an array does not do so. The common rule is that a .NET class only implements IDisposable when it manages resources other than memory. Or if it has a field of a class type that itself implements IDisposable.

在这种情况下是否应该实现析构函数也是个问题.您的示例类持有一个相当适中的非托管资源.通过实现析构函数,您可以将使用它的负担强加给客户端代码.这在很大程度上取决于类的使用情况,客户端程序员这样做的难易程度,绝对不是如果对象被期望存活很长时间,超出方法体以便使用 语句不可用.您可以让垃圾收集器知道它无法跟踪的内存消耗,调用 GC::AddMemoryPressure().这也解决了客户端程序员根本不使用 Dispose() 的情况,因为它太难了.

It is furthermore questionable whether you should implement a destructor in this case. Your example class is holding on to a rather modest unmanaged resource. By implementing the destructor, you impose the burden on the client code to use it. It strongly depends on the class usage how easy it is for the client programmer to do so, it definitely is not if the object is expected to live for a long time, beyond the body of a method so that the using statement isn't usable. You can let the garbage collector know about memory consumption that it cannot track, call GC::AddMemoryPressure(). Which also takes care of the case where the client programmer simply doesn't use Dispose() because it is too hard.

这篇关于C++/CLI 中重复的析构函数调用和跟踪句柄的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆