Изменения в семантике деструктора

Семантика для деструкторов класса значительно изменилась от управляемых расширений для C++ до Visual C++.

В управляемых расширениях разрешения деструктору класса предоставлялись ссылочным классом, но не классом значений. Это не изменилось и в новом синтаксисе. Тем не менее, семантика деструктора класса изменилась. В данном разделе обращается внимание на причины изменений и обсуждается их влияние на преобразование кода в среде CLR. Возможно это самое значительное изменение на уровне программистов между двумя версиями языка.

Недетерминированное завершение

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

Недетерминированное завершение хорошо работает с динамическим управлением памятью. Когда доступной памяти не хватает, вступает в дело сборщик мусора. В среде сборки мусора нет необходимости в том, чтобы деструкторы освобождали память. Однако недетерминированное завершение работает не очень хорошо, если объект поддерживает критические ресурсы, такие как соединение с базой данных или различные блокировки. В таком случае следует завершить работу над этим ресурсом как можно быстрее. В неуправляемой среде это достигается за счет применения пары конструктор-деструктор. Как только срок жизни объекта заканчивается, если заканчивается декларирование локального блока или если происходит развертывание стека по причине выданного исключения, выполняется деструктор, и ресурсы автоматически высвобождаются. Такой подход работает очень хорошо; ранее явно ощущалось его отсутствие в управляемых расширениях.

Решение, представленное средой CLR, заключается в том, чтобы класс реализовывал метод Dispose интерфейса IDisposable. Проблема здесь состоит в том, что метод Dispose требует явного вызова, выполненного пользователем. А это способствует появлению ошибок. Язык C# предоставляет недостаточный уровень автоматизации в виде специального оператора using. Структура управляемых расширений не предоставляет специальной поддержки.

Деструкторы в управляемых расширениях для C++

В управляемых расширениях деструктор ссылочного класса реализован согласно следующим двум этапам:

  1. Предоставленный пользователем деструктор внутренне переименовывается на Finalize. Если класс имеет базовый класс (помните, что в среде CLR Object Model, поддерживается только разовое наследование), компилятор вызывает метод завершения, который и выполняет предоставленный пользователем код. Рассмотрим следующую простую иерархию на примере спецификации управляемых расширений:
__gc class A {
public:
   ~A() { Console::WriteLine(S"in ~A"); }
};
   
__gc class B : public A {
public:
   ~B() { Console::WriteLine(S"in ~B");  }
};

В этом примере оба деструктора переименовываются в Finalize. При вызове метода B Finalize вызывается метод A Finalize, за которым следует вызов WriteLine. Это то, что сборщик мусора будет вызывать по умолчанию во время завершения. Вот как может выглядеть внутреннее преобразование:

// internal transformation of destructor under Managed Extensions
__gc class A {
public:
   void Finalize() { Console::WriteLine(S"in ~A"); }
};

__gc class B : public A {
public:
   void Finalize() { 
      Console::WriteLine(S"in ~B");
      A::Finalize(); 
   }
};
  1. На втором этапе компилятор синтезирует виртуальный деструктор. Пользовательские программы управляемых расширений вызывают данный деструктор либо напрямую, либо с помощью выражений delete. Он никогда не вызывается сборщиком мусора.

    В синтезированном деструкторе размещаются два оператора. Один для вызова GC::SuppressFinalize, чтобы убедиться в том, что больше нет вызовов метода Finalize. Второй является действительным вызовом метода Finalize, который представляет собой деструктор, предоставленный пользователем для этого класса. Вот как это может выглядеть:

__gc class A {
public:
   virtual ~A() {
      System::GC::SuppressFinalize(this);
      A::Finalize();
   }
};

__gc class B : public A {
public:
   virtual ~B() {
      System::GC::SuppressFinalize(this);
      B::Finalize();
   }
};

Эта реализация позволяет пользователю явно вызывать метод Finalize класса чаще, чем во времена, когда этим нельзя было управлять, но она не обеспечивает действительную привязку с решением метода Dispose. Это меняется в Visual C++.

Деструкторы в новом синтаксисе

В новом синтаксисе деструктор переименовывается внутренне в метод Dispose, а ссылочный класс автоматически расширяется, чтобы реализовать интерфейс IDispose. Вот как в Visual C++ трансформируется пара классов:

// internal transformation of destructor under the new syntax
__gc class A : IDisposable {
public:
   void Dispose() { 
      System::GC::SuppressFinalize(this);
      Console::WriteLine( "in ~A");
   }
};

__gc class B : public A {
public:
   void Dispose() { 
      System::GC::SuppressFinalize(this);
      Console::WriteLine( "in ~B");  
      A::Dispose(); 
   }
};

Когда деструктор вызывается явно в новом синтаксисе, либо когда delete применяется к обработке отслеживаний, обозначенный метод Dispose вызывается автоматически. Если это производный класс, вызов метода Dispose базового класса вставляется при закрытии синтезированного метода.

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

Декларирование ссылочного объекта

Visual C++ поддерживает объявление объекта ссылочного класса в локальном стеке или в качестве элемента класса так, как если он был доступен напрямую. В сочетании деструктора с методом Dispose результатом является автоматизированный вызов семантики завершения в ссылочных типах.

Во-первых, определяется ссылочный класс, такой что создание объекта функционирует в качестве выделения ресурса с помощью конструктора классов. Во-вторых, в деструкторе классов высвобождается ресурс, полученный при создании объекта.

public ref class R {
public:
   R() { /* acquire expensive resource */ }
   ~R() { /* release expensive resource */ }

   // … everything else …
};

Объект объявляется локально с помощью имени типа, но без соответствующей позиции. Все виды использования объекта, такие как вызов метода, выполняются с помощью элементного подбора точки (.) вместо стрелки (->). В конце блока связанный деструктор, трансформированный в Dispose, вызывается автоматически, как показано ниже:

void f() {
   R r; 
   r.methodCall();

   // r is automatically destructed here –
   // that is, r.Dispose() is invoked
}

Аналогично оператору using в C# этот пример не изменяет используемого ограничения среды CLR, что все типы ссылок должны располагаться в куче среды CLR. Используемая семантика остается неизменной. Пользователь мог бы точно также записать следующее (а это внутреннее преобразование, выполненное компилятором):

// equivalent implementation
// except that it should be in a try/finally clause
void f() {
   R^ r = gcnew R; 
   r->methodCall();

   delete r;
}

В действительности в соответствии с новым синтаксисом деструкторы работают в паре с конструкторами как автоматизированный механизм получения/освобождения, связанный со временем жизни объекта.

Декларирование явного метода завершения

В новом синтаксисе деструктор синтезируется в метод Dispose. Это означает, что если деструктор явно не вызывается, сборщик мусора во время завершения не найдет его, так как не будет найден ассоциированный метод Finalize для данного объекта. Чтобы поддержать разрушение и завершение, следует ввести специальный синтаксис для обеспечения метода завершения. Примеры.

public ref class R {
public:
   !R() { Console::WriteLine( "I am the R::finalizer()!" ); }
};

Префикс ! аналогичен тильде (~), которая вводит деструктор класса, т.е. оба метода, завершившие свое существование, имеют маркировочный префикс данного класса. Если синтезированный метод Finalize применяется в производном классе, вызов метода базового класса Finalize вставляется в конце. Если деструктор явно вызван, метод завершения подавляется. Вот как выглядит преобразование:

// internal transformation under new syntax
public ref class R {
public:
   void Finalize() {
      Console::WriteLine( "I am the R::finalizer()!" );
   }
}; 

Переход от управляемых расширений для C++ к Visual C++ 2010

Поведение среды выполнения программы управляемых расширений для C++ меняется после компиляции в Visual C++ независимо от того, содержит ли ссылочный класс необычный деструктор. Требуемый алгоритм трансформации выглядит следующим образом:

  1. При наличии деструктора необходимо переписать это в качестве метода завершения класса.

  2. При наличии метода Dispose можно переписать это в качестве деструктора класса.

  3. Если имеется деструктор, но нет метода Dispose, сохраните деструктор во время выполнения первой операции.

При переходе кода из управляемых расширений в новый синтаксис можно пропустить выполнение этого преобразования. Если приложение в некоторой степени зависимо от выполнения связанных методов завершения, поведение приложения будет непредсказуемым.

См. также

Ссылки

Деструкторы и методов завершения в Visual C++

Основные понятия

Управляемые типы (C++/CL)