Вопрос по c++, c++-cli, garbage-collection – Повторные вызовы деструктора и отслеживание маркеров в C ++ / CLI

14

Я играю с C ++ / CLI, используя документацию MSDN иСтандарт ECMAи Visual C ++ Express 2010. Меня поразил следующий отход от 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.

Я придумал маленький пример:

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

Questions:

Is it undefined behavior, or is it entirely acceptable, to call delete r on line #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.

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

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

Другими словами, управляемые объекты «живут вечно» и могут ли их деструкторы и их финализаторы вызываться снова и снова?

В ответ на требование @ Hans к нетривиальному классу вы также можете рассмотреть эту версию (с деструктором и финализатором, выполненными в соответствии с требованием множественного вызова):

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;
};
@ HansPassant: Так в чем же смысл этого стандартного предложения? Kerrek SB
Печать строк в методах имеет мало общего с тем, что на самом деле делает среда выполнения. Смысл написания деструктора и финализатора в том, чтобы на самом деле сделать что-то значимое. Да, вам разрешено называть это ...> Фу, вот сила. Это на самом деле не имеет ничего общего с правилом, которое GC использует для вызова финализатора. Направьте пистолет на ногу, нажмите курок. Настоящий код умирает на NRE или AV. Hans Passant

Ваш Ответ

2   ответа
16

Я просто попытаюсь решить вопросы, которые вы затронули, по порядку:

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 обычно выполняет свою работу.

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

Это не UB и полностью приемлемо.delete Оператор просто вызывает метод IDisposable :: Dispose () и таким образом запускает ваш деструктор. То, что вы делаете внутри, обычно вызывая деструктор неуправляемого класса, вполне может вызвать UB.

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

Нет. Вызывать деструктор совершенно необязательно без хорошего способа его применения. Ничто не идет не так, финализатор в конечном итоге всегда будет работать. В данном примере это произойдет, когда CLR запустит поток финализатора в последний раз перед завершением работы. Единственным побочным эффектом является то, что программа запускается «тяжело», удерживая ресурсы дольше, чем необходимо.

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

Это довольно распространенное явление: чрезмерно усердный программист C # вполне может вызывать ваш метод Dispose () более одного раза. Классы, которые предоставляют как методы Close, так и Dispose, довольно распространены в фреймворке. Есть некоторые модели, где это почти неизбежно, случай, когда другой класс предполагает владение объектом. Стандартный пример - это бит кода C #:

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

Объект StreamWriter получит владение своим базовым потоком и вызовет его метод Dispose () в последней фигурной скобке.using оператор объекта FileStream вызывает Dispose () во второй раз. Написание этого кода таким образом, чтобы этого не происходило и все же обеспечивало гарантии исключений, слишком сложно. Указание, что Dispose () может быть вызван несколько раз, решает проблему.

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

Все хорошо. Результат вряд ли будет приятным, вероятнее всего, будет NullReferenceException. Это то, что вы должны проверить, вызвать исключение ObjectDisposedException, чтобы дать программисту лучшую диагностику. Все стандартные классы .NET Framework делают это.

In other words, do managed objects "live forever"

Нет, сборщик мусора объявляет объект мертвым и собирает его, когда больше не может найти никаких ссылок на объект. Это отказоустойчивый способ управления памятью, нет возможности случайно сослаться на удаленный объект. Потому что для этого требуется ссылка, которую всегда будет видеть GC. Общие проблемы управления памятью, такие как циклические ссылки, также не являются проблемой.

Code snippet

Удалениеa объект не нужен и не имеет никакого эффекта. Вы удаляете только те объекты, которые реализуют IDisposable, а массив этого не делает. Общее правило заключается в том, что класс .NET реализует IDisposable только тогда, когда он управляет ресурсами, отличными от памяти. Или если у него есть поле типа класса, которое само реализует IDisposable.

Кроме того, сомнительно, следует ли вам применять деструктор в этом случае. Ваш пример класса держится за довольно скромный неуправляемый ресурс. Реализуя деструктор, вы накладываете на клиентский код бремя его использования. От использования класса сильно зависит то, насколько легко это сделать клиентскому программисту, это определенно не так, если ожидается, что объект будет жить долго, вне тела метода, так чтоusing заявление не может быть использовано. Вы можете сообщить сборщику мусора о потреблении памяти, которое он не может отследить, вызвав GC :: AddMemoryPressure (). Который также учитывает случай, когда клиентский программист просто не использует Dispose (), потому что это слишком сложно.

+1 - но & quot; Финализатор в конечном итоге всегда запускается & quot; ложно Увидетьblogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Спасибо, очень хороший ответ! Kerrek SB
Вопрос стиля: Вы бы порекомендовали использовать автоматические объекты, когда это возможно, независимо от того, являются ли они управляемыми или неуправляемыми? Мне кажется, что наличие деструктора (как в моем фрагменте кода) не является проблемой, если мы используем только автоматические переменные. И мы можем даже написатьunique_cli_ptr шаблон :-) Kerrek SB
@Kerrek: Я бы использовал его только для взаимодействия модулей. И даже тогда, только если вам нужно взаимодействовать с классами C ++. Если вам нужно взаимодействовать только с C, тогда я буду использовать P / Invoke илиunsafe блоки вместо этого в C #. C ++ / CLI позволяет слишком легко облажаться и использовать указатель где-то на земле CLR, что мгновенно делает весь ваш модуль неспособным работать в средах с частичным доверием, таких как Silverlight, Windows Phone 7 и т. Д. Это также может сбивать с толку из-за его & APOS; «Двойной взгляд на мир» что вы видите здесь. Есть биты CLR и собственные биты, и семантика очень различна.
@Kerrek: Нет, потому что если это & quot; автоматический объект & quot; вызывается кем-либо кроме C ++ / CLI (например, C #), семантика, которую вы хотите, трудна для этого кода. C ++ / CLI естьnot C ++. Код в C ++ / CLI выглядитfar больше похоже на C #, потому что идиомы и тому подобное приводятся в действие CLR, а не традиционными идеями из C ++.
1

Рекомендации из стандарта C ++ все еще применяются:

  1. Calling delete on an automatic variable, or one that's already been cleaned up, is still a bad idea.

  2. It's a tracking pointer to a disposed object. Dereferencing such is a bad idea. With garbage collection, the memory is kept around as long as any non-weak reference exists, so you can't access the wrong object by accident, but you still can't use this disposed object in any useful way, since its invariants probably no longer hold.

  3. Multiple destruction can only happen on managed objects when your code is written in really bad style that would have been UB in standard C++ (see 1 above and 4 below).

  4. Explicitly calling the destructor on an automatic variable, then not creating a new one in its place for the automatic destruction call to find, is still a bad idea.

В общем, вы думаете, что время жизни объекта отделено от выделения памяти (как это делает стандартный C ++). Сборка мусора используется для управления освобождением - поэтому память все еще там - но объект мертв. В отличие от стандартного C ++, вы не можете использовать эту память для хранения необработанных байтов, поскольку части среды выполнения .NET могут предполагать, что метаданные все еще действительны.

Ни сборщик мусора, ни "семантика стека" (автоматический синтаксис переменной) использовать подсчет ссылок.

(Некрасивые детали: удаление объекта не нарушает собственных инвариантов среды выполнения .NET, касающихся этого объекта, так что вы, вероятно, даже можете использовать его как монитор потоков. Но это просто делает уродливый, трудный для понимания дизайн, поэтому пожалуйста, не делайте.)

Это не объект C ++.ref classОни никогда не являются объектами C ++ и не следуют правилам C ++.
Код, отличный от C ++ / CLI, может вызыватьref class типы. Таким образом, он должен следовать семантике CLR, а не семантике C ++. Семантика CLR позволяет вызывать Dispose несколько раз; следовательно правильно написанный C ++ / CLIref class Также необходимо разрешить утилизацию вызываться несколько раз. Даже C ++ / CLI заставляет dispose вызываться более одного раза на регулярной основе; например если есть поток и TextReader, где TextReader владеет потоком, где они обе являются автоматическими переменными, C ++ / CLI дважды вызывает dispose для потока.
2: не верно на CLR. Семантика удаленного объекта - это объект, который все еще существует на земле CLR. 3. На самом деле, если ваш код вызывается из C #, существует ряд общих шаблонов, которые заставляют множественные вызовы постоянно утилизироваться.
@Billy: Насколько это возможно, компилятор C ++ обеспечивает соблюдение правил CLR и C ++ для управляемых типов, определенных в C ++ / CLI. В любом случае гораздо проще рассуждать только об использовании объектов между вызовом конструктора и деструктора, поэтому вопрос о том, для чего этот объект хорош после уничтожения, является спорным вопросом.
@Billy: время жизни объекта закончилось в смысле C ++ (деструктор запустился). Может существовать базовый объект CLR, все еще существующий, но рассматривать его как объект C ++ было бы плохо.

Похожие вопросы