Sdílet prostřednictvím


Sémantické změmy v destruktoru

Sémantika pro destruktory třídy se oproti spravovanému rozšíření jazyka C++ v aplikaci Visual C++ 2010 významně změnila.

Ve spravovaném rozšíření byl destruktor povolen v rámci referenční třídy ale ne v rámci hodnotové třídy. To se v nové syntaxi nezměnilo. Nicméně semantika destruktoru třídy se změnila. Toto téma se zaměřuje na důvody těchto změn a popisuje jak ovlivňují překlad existujícího kódu modulu CLR. Je to pravděpodobně nejdůležitější změna úrovně programování mezi dvěmi verzemi jazyka.

Nedeterministické dokončení

Předtím, než je paměť přiřazena objektu, je zažádáno systémem uvolňování paměti o vyvolání přidružené metody Finalize, pokud je k dispozici. Tuto metodu si lze představit jako druh super-destruktoru, protože není vázána k programové životnosti objektu. Tomu se říká dokončení. Okamžik kdy nebo zda má být vyvolána metoda Finalize není definován. Tím je myšleno, když říkáme, že systém uvolňování paměti vykazuje nedeterministicé dokončení.

Nedeterministické dokončení dobře spolupracuje se správou dynamické paměti. Jakmile se volná paměť stane nedostatečnou, zasáhne systém uvolňování paměti. V prostředí systému uvolňování paměti jsou destruktory pro uvolnění paměti zbytečné. Nedeterministické dokončení nepracuje správně, pokud objekt udržuje kritické zdroje jako je například připojení k databázi nebo určitý druh zámku. V takovém případě by jsme měli tento zdroj uvolnit co nejdříve. Toho je normálně dosaženo, pokud je použito páru konstruktor/destruktor. Co nejdříve po ukončení platnosti objektu, buď po skončení místního bloku ve kterém je objekt deklarován nebo když je rozdělen zásobník z důvodu výjimky, zasáhne destruktor a automatiky uvolní prostředek. Tento přístup funguje velmi dobře a jeho nepřítomnost pod spravovaným rozšířením byla příliš opomíjená.

Řešení poskytnuto modulem CLR bylo implementovat pro třídu metodu Dispose rozhraní IDisposable. Problémem je to, že metoda Dispose vyžaduje explicitní volání uživatelem. Toto je náchylné k chybě. Jazyk C# poskytuje mírnou formu automatizace ve formě speciálního příkazu using. Design spravovaného rozšíření neposkytuje žádnou zvláštní podporu.

Destruktory ve spravovaném rozšíření jazyka C++

Ve spravovaném rozšíření je destruktor referenční třídy implementován pomocí následujících dvou kroků:

  1. Uživatelem zadaný destruktor je interně přejmenován na Finalize. Pokud má třída základní třídu (nezapomeňte, že u modelu CLR objektu je podporována pouze jediná dědičnost), zanese kompilátor volání do své finalizační metody a následně provede kód uživatele. Zvažte například následující jednoduchou hierarchii převzatou z jazykové specifikace spravovaného rozšíření:
__gc class A {
public:
   ~A() { Console::WriteLine(S"in ~A"); }
};
   
__gc class B : public A {
public:
   ~B() { Console::WriteLine(S"in ~B");  }
};

V tomto příkladu jsou oba destruktory přejmenované Finalize. Metoda Finalize třídy B vyvolá metodu Finalize třídy A následovanou přidáním volání metody WriteLine. Tohle systém uvolňování paměti vyvolá ve výchozím nastavení během dokončení. Tato vnitřní transformace může vypadat například takto:

// 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. Ve druhém kroku kompilátor syntetizuje virtuální destruktor. Tento destruktor je to co naše uživatelské aplikace spravovaného rozšíření vyvolají buď přímo nebo skrz aplikaci ke smazání výrazu. Nikdy není vyvolán systémem uvolňování paměti.

    V rámci syntetizovaného destruktoru jsou umístěny dva příkazy. Jedním je voláno GC::SuppressFinalize, k ujištění se, že už nejsou žádná další volání metody Finalize. Druhým je aktuálně vyvolána metoda Finalize, která představuje uživatelem zadaný destruktor pro danou třídu. Zde je ukázáno, jak to může vypadat:

__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();
   }
};

Zatímco tato implementace umožňuje uživateli explicitně volat, dnes spíše než dříve když jste nad tím neměli kontrolu, metodu třídy Finalize, už opravdu nestačí použít řešení propojení s metodou Dispose. Toto je změněno v aplikaci Visual C++ 2010.

Destruktory v nové syntaxi

V nové syntaxi je destruktor přejmenován interně na metodu Dispose a referenční třída je automaticky rozšířena tak, aby implementovala rozhraní IDispose. To znamená, že se v rámci aplikace Visual C++ 2010, transformuje naše dvojice tříd takto:

// 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(); 
   }
};

Když je jeden destruktor volán explicitně novou syntaxí nebo když je aplikován příkaz delete ke sledování popisovače, je volána automaticky výchozí metoda Dispose. Pokud se jedná o odvozenou třídu, je volání metody Dispose základní třídy vloženo na konec syntetizované metody.

To ale není vše k získání deterministického dokončení. Chcete-li toho dosáhnout, budete potřebovat dodatečnou podporu místních referenčních objektů. (Toto nemá žádnou analogickou podporu v rámci spravovaného rozšíření a tak se nejedná o problém s překladem.)

Deklarování referenčního objektu

Aplikace Visual C++ 2010 podporuje deklaraci objektu referenční třídy na místním zásobníku nebo jako člena třídy aby byl přímo přístupný. V kombinaci přidružení destruktoru s metodou Dispose, je výsledkem automatické vyvolání dokončovací sémantiky pro referenční typy.

Nejprve definujeme naši referenční třídu tak, aby funkce vytvoření objektu měla přínos k získání zdroje skrze konstruktor třídy. Za druhé, uvolníme v rámci destruktoru třídy zdroj, záskaný při vytvoření objektu.

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

   // … everything else …
};

Objekt je deklarován místně pomocí názvu typu, ale bez doprovodného pokrytí. Všechna použití objektu, jako je volání metody, jsou prováděny prostřednictvím výběru člena tečkou (.) namísto šipkou (->). Na konci bloku, je přidružený destruktor transformován do metody Dispose, vyvolán automaticky. Tak jak je zde znázorněno:

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

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

Jako s použitím příkazu using v rámci jazyka C#, nebude toto porušovat základní omezení modulu CLR, že všechny referenční typy musí být přiděleny na haldě modulu CLR. Základní sématika zůstává nezměněna. Uživatel může ekvivalentně napsat následující (a to je pravděpodobně vnitřní transformace prováděná kompilátorem):

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

   delete r;
}

Ve skutečnosti jsou podle nové syntaxe destruktory opět svázány s konstruktory jako automatický získávací/uvloňovací mechanismus, vázaný na dobu života místního objektu.

Deklarování explicitního dokončení

Jak jsme viděli, tak v nové syntaxi je destruktor syntetizován do metody Dispose. To znamená, že když není destruktor explicitně vyvolán, systémem uvolňování paměti, během dokončení, nebude nalezena přidružená metoda Finalize pro objekt. K podpoře jak destrukcí, tak dokončování jsme zavedli speciální syntaxi pro poskytování finalizační metody. Příklad:

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

Předpona ! je analogická k tildě (~), která zavádí destruktor třídy – to znamená, že obě metody starající se o objekt po jeho platnosti, mají tento znak jako předponu názvu třídy. Pokud syntetizovaná metoda Finalize vychází z odvozené třídy, je vyvolání metody Finalize základní třídy, vloženo na jejím konci. Pokud je destruktor explicitně vyvolán, je finalizační metoda potlačena. Takto by mohla vypadat zmíněná transformace:

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

Přesunutí ze spravovaného rozšíření jazyka C++ na Visual C++ 2010

Chování běhu programu ze spravovaného rozšíření jazyka C++ se změní, když je program přeložen v aplikaci Visual C++ 2010 a pokud referenční třída obsahuje složitý destruktor. Požadováný algoritmus překladu je podobný následujícímu:

  1. Je-li přítomen destruktor, přepiš ho na finalizační metodu třídy.

  2. Pokud je k dispozici metoda Dispose, přepiš ji do destruktoru třídy.

  3. Jestliže je k dispozici destruktor, ale není zde žádná metoda Dispose, ponechej destruktor při plnění prvního bodu.

Při přesunutí vašeho kódu ze spravovaného rozšíření na novou syntaxi, můžete provádění této transformace vynechat. Pokud aplikace nějakým způsobem závislá na provádění přidružených finalizačních metod, chování aplikace se oproti tomu, co jste zamýšleli, nepatrně změní.

Viz také

Odkaz

Destructors and Finalizers in Visual C++

Koncepty

Typy spravovaných (C + +/ CL)