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

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

问题描述

我正在使用C ++ / CLI,使用MSDN文档和 ECMA标准<对我有用[0]丢个板砖[0]引用举报返回列表发新帖高级模式B Color Image Link Quote Code Smilies您需要登录后才可以回帖登录|析构函数必须写成可以多次执行和未完全构造的对象。


我编写了一个例子:

  #include< iostream> 

ref struct Foo
{
Foo(){std :: wcout< LFoo()\\\
; }
〜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 !输出为:

  Foo()
〜Foo b!Foo()
〜Foo()
!Foo()

问题:


  1. 是未定义的行为还是完全可以接受, $ c> delete r 上线#2


  2. 删除线#2 ,是否重要的​​ r 仍然是一个对象的跟踪句柄C ++)不再存在?这是一个悬挂手柄吗?



    我知道没有一个实际的双删除,因为输出变为:

      Foo()
    〜Foo()
    !Foo b

    但是,我不确定这是一个幸福的事故还是保证明确定义的行为。


  3. b
  4. 之前或之后插入 x。〜Foo(); code>?


换句话说,管理对象永远活着






为了回应@ Hans对一个不琐碎的类的需求,你可以也考虑这个版本(使析构函数和终结器符合多个调用的要求):

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

〜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;
};


解决方案

我会尽力解决您遇到的问题


对于ref类,必须编写终结器和析构函数,以便它们可以多次执行,未完全构建。


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


是未定义的行为,在第#2行删除r?


这不是UB,完全可以接受。 delete 运算符只调用IDisposable :: Dispose()方法,因此运行您的析构函数。


如果我们移除第2行,那么你可以调用UB的析构函数。是否重要,r仍然是跟踪句柄


否。调用析构函数是完全可选的,没有一个好的方法来强制它。没有什么问题,终结者最终将永远运行。在给定的示例中,当CLR在关闭之前最后一次运行终结器线程时将发生。唯一的副作用是程序运行重,持续资源超过必要。


在其他情况下,可以多次调用受管对象的析构函数?




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

  using(var fs = new FileStream(...))
使用(var sw = new StreamWriter(fs)){
//写入文件...
}

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


插入x。〜Foo紧接在r =%x之前或之后;?


没关系。结果不太可能是令人愉快的,NullReferenceException将是最可能的结果。这是你应该测试,提出一个ObjectDisposedException,给程序员一个更好的诊断。所有标准的.NET框架类都是这样做的。


换句话说,托管对象永远活着


不,垃圾回收器声明对象死了,并且当它不能再找到对象的任何引用时,收集它。这是一种故障安全的内存管理方法,没有办法意外引用已删除的对象。因为这样做需要一个引用,GC将总是看到。常见内存管理问题(如循环引用)也不是问题。


代码段


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



在这种情况下是否应该实现析构函数是更加可疑的。你的示例类持有一个相当温和的非托管资源。通过实现析构函数,您对客户端代码施加了使用它的负担。它强烈依赖于类使用对于客户端程序员这么做有多么容易,它绝对不是如果对象预期生活很长时间,超出方法的主体,使得使用语句不可用。你可以让垃圾收集器知道它无法跟踪的内存消耗,调用GC :: AddMemoryPressure()。这也顾虑了客户端程序员不使用Dispose()的情况,因为它太难了。


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++:

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.

I concocted a little example:

#include <iostream>

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

int main()
{
    Foo ^ r;

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

    delete r;      // #2
}

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()

Questions:

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

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

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

  4. 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?


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()\n";
    }

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

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

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

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

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

解决方案

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

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.

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.

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

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.

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

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?

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

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.

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

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"

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.

Code snippet

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.

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天全站免登陆