デストラクターのセマンティクスの変更

クラス デストラクターのセマンティクスは、Visual C++ 2010 では C++ マネージ拡張から大幅に変更されています。

マネージ拡張では、クラス デストラクターは参照クラスでは使用が許可されていますが、値クラスでは使用が許可されていません。 この点については、新しい構文でも変更されていません。 ただし、クラス デストラクターのセマンティクスは変更されています。 このトピックでは、その変更の理由と、既存の CLR コードの変換に及ぼす影響について説明します。 この点は、2 つのバージョンの言語間でなされた変更の中でも、プログラマにとって最も重要な部分であると思われます。

非確定的な終了処理

オブジェクトに関連付けられているメモリがガベージ コレクターによって再要求される前には、関連付けられている Finalize メソッドがあればそれが呼び出されます。 このメソッドは、オブジェクトのプログラム内の存続期間に結び付いていないため、一種の特殊なデストラクターと考えることができます。 これを終了処理と呼びます。 Finalize メソッドは、いつ呼び出されるかだけでなく、呼び出されるかどうかさえもはっきりしません。 ガベージ コレクションの動作が非確定的な終了処理と呼ばれる意味はここにあります。

非確定的な終了処理は、動的なメモリ管理と連携して機能します。 使用可能なメモリが不足してくると、ガベージ コレクターが始動します。 ガベージ コレクターが作用している環境では、メモリを解放するためのデストラクターは不要です。 しかし、非確定的な終了処理が機能しない場合もあります。データベース接続やなんらかの種類のロックなど、オブジェクトが重大なリソースを保持している場合がそうです。 そのような場合は、そのリソースをできるだけ早く解放する必要があります。 ネイティブ実装では、コンストラクターとデストラクターが、その役割を果たすことになります。 オブジェクトが宣言されたローカル ブロックが終了するか、例外がスローされてスタックが戻されることによって、オブジェクトの存続期間が終わると、すぐにデストラクターが呼び出されてリソースが自動的に解放されます。 このしくみは実に効果的であり、マネージ拡張に含まれなかったのは非常に残念でした。

CLR によって提供されているソリューションは、クラスで IDisposable インターフェイスの Dispose メソッドを実装するというものです。 ここで問題になるのが、Dispose はユーザーが明示的に呼び出す必要があるということです。 この点がエラーを招きやすくしています。 C# 言語では、特殊な using ステートメントを使用することによって目立たない形でこれを自動化できます。 マネージ拡張の設計では、そのための特殊な対応がなされていませんでした。

C++ のマネージ拡張のデストラクター

マネージ拡張では、参照クラスのデストラクターは次の 2 つの手順によって実装されます。

  1. ユーザーが指定したデストラクターの名前が内部で Finalize に変更されます。 また、クラスに基本クラスがある場合は (CLR のオブジェクト モデルでは、単一の継承のみがサポートされるのを忘れないでください)、ユーザーが指定したコードの後に基本クラスのファイナライザーの呼び出しが挿入されます。 例として、マネージ拡張の言語仕様に含まれている次の簡単な階層構造について見てみましょう。
__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 の WriteLine の呼び出しの後に、A の Finalize メソッドの呼び出しが追加されます。 これが、終了処理の際にガベージ コレクターによって既定で呼び出されます。 この内部の変換のようすを次に示します。

// 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. 2 つ目の手順では、コンパイラによって仮想デストラクターが合成されます。 このデストラクターは、マネージ拡張のユーザー プログラムによって直接呼び出されるか、delete 式が適用された結果として呼び出されます。 ガベージ コレクターによって呼び出されることはありません。

    2 つのステートメントは、この合成されたデストラクター内に配置されます。 1 つは、もうそれ以上 Finalize が呼び出されないようにするための GC::SuppressFinalize の呼び出しです。 2 つ目は、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++ 2010 では変更されています。

新しい構文でのデストラクター

新しい構文では、デストラクターの名前は内部で Dispose メソッドに変更され、参照クラスが自動的に拡張されて IDispose インターフェイスが実装されます。 したがって、先ほどのクラスのペアは、Visual C++ 2010 では次のように変換されます。

// 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++ 2010 では、参照クラスのオブジェクトの宣言をローカル スタックで、または直接アクセスできるかのようにクラスのメンバーとしてサポートしています。 これを、デストラクターと 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
}

これは、C# の using ステートメントと同じような構文であり、参照型はすべて CLR ヒープに割り当てる必要があるという、基になる CLR の制約と対立するものではありません。 基になるセマンティクスは変更されていません。 次のように記述しても同じことになります (おそらくは、このような変換がコンパイラによって内部で行われているものと思われます)。

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

   delete r;
}

要するに、新しい構文では、ローカル オブジェクトの存続期間に結び付いた自動的な取得/解放の機構として、再びデストラクターとコンストラクターの組み合わせが使用されています。

明示的な Finalize の宣言

新しい構文では、これまで見てきたように、デストラクターから 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++ 2010 でコンパイルした場合、参照クラスに重要なデストラクターが含まれていると、実行時の動作が変わります。 必要になると思われる変換アルゴリズムを次に示します。

  1. デストラクターが存在する場合は、クラス ファイナライザーに書き換えます。

  2. Dispose メソッドが存在する場合は、クラス デストラクターに書き換えます。

  3. デストラクターは存在するが Dispose メソッドがない場合は、デストラクターを残したまま最初の項目を実行します。

マネージ拡張から新しい構文に移行する際には、この変換を実行するのを忘れないようにしてください。 そうしないと、関連付けられている終了処理メソッドの実行にアプリケーションがなんらかの形で依存している場合に、アプリケーションの動作が意図に反して知らないうちに変更されることになります。

参照

参照

Destructors and Finalizers in Visual C++

概念

マネージ型 (C++/CL)