重复析构函数调用和C ++ / CLI跟踪句柄(Repeated destructor calls

2019-06-27 17:56发布

我与C ++ / CLI玩耍,使用MSDN文档和ECMA标准和Visual C ++ 2010年快递什么我吃惊的是,从C ++以下出发:

对于裁判班,无论是终结和析构函数必须写成这样他们就可以被执行多次,并在尚未完全构造的对象。

我编造了一个小例子:

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

在该块的末#1 ,自动变量x模具,和析构函数被调用(这反过来调用终结明确地,由于是通常的成语)。 这是一切优秀和良好。 但后来我通过基准再次删除对象r 输出是这样的:

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

问题:

  1. 它是未定义的行为,或者是完全可以接受,调用delete r线#2

  2. 如果我们以删除线#2 ,它的问题是r仍然为(在C ++感)不再存在的对象跟踪处理? 它是一个“晃来晃去处理”? 难道它的引用计数意味着将有企图双缺失?

    我知道,没有一个实际的双重缺失,作为输出变成这样:

     Foo() ~Foo() !Foo() 

    但是,我不知道这是否是一个快乐的事故或保证是良好定义的行为。

  3. 在其其他情况下可以被管理对象的析构函数被调用一次以上?

  4. 难道是OK插入x.~Foo(); 紧接之前或之后r = %x;

换句话说,你的管理对象“长生不老”,可以同时拥有自己的析构函数及其终结叫了一遍又一遍?


为了响应@汉斯对一个不平凡的类的需求,你也可以考虑这个版本(与析构函数和终结作出符合多个呼叫要求):

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

Answer 1:

我只是试图解决您带来了,为了这些问题:

对于裁判班,无论是终结和析构函数必须写成这样他们就可以被执行多次,并在尚未完全构造的对象。

析构函数~Foo()简单地自动生成两个方法中,所述的IDisposable的实现:: Dispose()方法以及它实现了一次性图案的保护的Foo ::的Dispose(布尔)方法。 这些是普通的方法,因此可以被调用多次。 它允许在C ++ / CLI直接调用终结, this->!Foo()和一般做,就像你一样。 垃圾收集器永远只能调用终结一次,它使内部跟踪不论这是否已完成。 鉴于主叫终结直接被允许,并且调用Dispose()多次被允许,因此可以运行终结代码一次以上。 这是专门针对C ++ / CLI,其他托管语言不答应。 你可以很容易地阻止它,一个nullptr检查通常能够完成任务。

它是未定义的行为,或者是完全可以接受的,叫上线#2删除R'

它不是UB和完全可以接受。 该delete操作员只需调用了IDisposable :: Dispose()方法,从而运行您的析构函数。 你做什么里面,很典型的调用非托管类的析构函数,可能调用UB。

如果我们以删除线#2,它的问题是R还是一个跟踪手柄

号调用析构函数是没有强制执行的好方法完全是可选的。 一切正常,终结最终将始终运行。 在给定的例子,当CLR运行终结器线程最后一次关机前会发生。 唯一的副作用就是程序运行“重”,抱着一种资源超过所需的时间。

在其其他情况下可以被管理对象的析构函数被调用一次以上?

这是很常见的,一个过分热心的C#程序员很可能打电话给你的Dispose()方法不止一次。 同时提供一个关闭和Dispose方法类是在框架中相当普遍。 有一定的模式,其中它几乎是不可避免的,在另一个类假定有一个对象的所有权的情况下。 标准的例子是该位的C#代码:

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

该StreamWriter对象将它的基本流的所有权,并在最后的大括号调用其Dispose()方法。 FileStream对象上的using语句调用Dispose()第二次。 写这个代码,以便不会发生这种情况,并仍然提供担保的例外是太困难了。 指定的Dispose()可以被称为不止一次地解决了这个问题。

难道是OK插入X〜美孚()。 紧接之前或之后的R =%×;?

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

换句话说,做管理对象“长生不老”

不,垃圾收集声明的对象死亡,并收集它,当它不能找到对象的任何引用了。 这是一个故障安全方式的内存管理,也没有办法意外引用删除的对象。 因为这样做需要一个参考,一个是,GC总会看到。 像循环引用常见的内存管理问题也不是问题。

代码片段

删除a对象是不必要的,并且没有任何影响。 你只能删除实现IDisposable对象,数组没有这样做。 一般规律是,当它管理内存以外资源的.NET类仅实现了IDisposable。 或者,如果它有本身实现IDisposable类类型的字段。

这是另外值得怀疑,你是否应该实施这种情况下,析构函数。 您的示例类是抱着一个相当温和的非托管资源。 通过实施析构函数,你强加给客户端代码的负担使用它。 这在很大程度上取决于使用类是多么容易为客户程序员这样做,这绝不是如果对象预期存活时间长,超出方法体,使using语句无法使用。 你可以让垃圾收集了解内存消耗,它不能跟踪,调用GC :: AddMemoryPressure()。 这也需要的情况下照顾在客户端程序员根本没有使用的Dispose(),因为它实在是太难了。



Answer 2:

从标准C ++的准则仍然适用:

  1. 调用delete的自动变量,或者一个一个已经被清理,仍然是一个坏主意。

  2. 这是一个跟踪指针释放的对象。 解引用这样是一个坏主意。 随着垃圾回收,内存保持在大约只要任何非弱引用存在,所以你不能意外访问错了对象,但是您仍然无法很可能在任何有用的方式使用该释放的对象,因为它的不变量不再持有。

  3. 多重破坏只能在管理对象发生时,你的代码是用非常糟糕的风格,会一直在UB标准C ++(见上文1和4下文)。

  4. 显式调用析构函数的自动变量,然后在其位不创建一个新的自动销毁打电话找,仍然是一个坏主意。

一般情况下,你认为对象生存视为从存储器分配(就像标准C ++一样)是分开的。 垃圾回收用于管理释放 - 所以内存是仍然存在 - 但对象已经死了。 不同于标准C ++,你不能去和重用的原始字节的存储内存,因为.NET运行库的部分可以假定元数据仍然有效。

无论是垃圾收集器,也不是“堆语义”(自动可变语法)使用引用计数。

(丑陋的细节:设置一个对象不破.NET运行时自身的关于该对象不变,所以你可能甚至还可以把它当作一个线程监控,但只是让一个丑陋难以了解设计,所以请不要。 “T)。



文章来源: Repeated destructor calls and tracking handles in C++/CLI