執筆者: エディフィストラーニング株式会社 矢嶋 聡


目次

  1. はじめに
  2. 今回作成するサンプル アプリケーション
  3. .NET 向けの基本的な実装 ~単純なラッパーではない点に注意~
  4. ラッパーのためのハイブリッド型のプロジェクトの準備
  5. ラッパー クラスの作成
  6. ラッパー クラスの公開 ~アクセス修飾子の注意点~
  7. 各メンバーの定義
  8. 後処理の注意点 ~デストラクターとファイナライザー~
  9. デストラクターで実装すべきこと
  10. ファイナライザーで実装すべきこと
  11. サンプルの利用
  12. まとめ

1. はじめに

今回は前回に引き続き、C++/CLI (Common Language Infrastructure) について取り上げます。前回では、次図の C++/CLI を利用した実装パターンを挙げて、それぞれのメリットやデメリットについて説明したのち、主に (3) のパターンを説明しました。今回は (4) のパターンを取り上げます。

図 9.1

図 9.1 前回取り上げた Visual C++ における .NET Framework 関連の実装パターン

この (4) のパターンは、C++ ネイティブ コードと C++/CLI によるマネージ コードを 1 つのアプリケーションの中で併用する「ハイブリッド型」の 1 つであり、ネイティブな C++ で記述されたクラスなどの既存資産を、他の .NET 対応アプリケーションから利用するための形態です。

今回は、このような形態において、C++ 側での実装の特徴や注意点などを中心に、具体的なサンプルを用いながら確認していきます。

Note:

C++/CLI の言語構文の詳細は、以下を参照してください。

ページのトップへ


2. 今回作成するサンプル アプリケーション

今回のサンプルでは、既存のソフトウェア資産として、ネイティブ コード版の C++ のクラスが既にあることを前提にしており、このクラスを .NET 対応アプリケーションから再利用できるようにするため、C++/CLI を用いて、必要な実装を追加します (プロジェクトの作り方や入力方法は後述します)。

例 9.1 今回題材にする既存資産としてのネイティブ コード版の C++ クラス

ファイル名: FileCache.h

C++
スクリプトの編集|Remove
#pragma once 
 
#define FILECACHE_OUTOF_RANGE (-1)  //インデックス範囲外のエラーコード 
 
// ファイルをメモリにキャッシュ 
class CFileCache 
{ 
private: 
    char* m_pData;  //メモリ上のバッファポインター 
    int m_nLength;  //メモリ上のデータサイズ(バイト数) 
public: 
    CFileCache(void);  //コンストラクター 
    ~CFileCache(void); //デストラクター 
private: 
    BOOL AllocData(int size); //メモリ確保、メンバー変数設定 
    void ClearData();         //メモリ解放、メンバー変数初期化 
public: 
    BOOL Load(LPCWSTR filePath);  //ファイル読み込み 
    char operator[](int ndx);     //インデックスでのアクセス 
    int GetLength() { return m_nLength; } //データサイズの取得(バイト数) 
};

ファイル名: FileCache.cpp

C++
スクリプトの編集|Remove
#include "StdAfx.h" 
#include "FileCache.h" 
 
CFileCache::CFileCache(void) 
    : m_pData(NULL), m_nLength(0) 
{ 
} 
 
CFileCache::~CFileCache(void) 
{ 
    ClearData(); 
} 
 
BOOL CFileCache::AllocData(int size) 
{ 
    m_pData = (char*) ::HeapAlloc( 
        ::GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)size); 
    if(m_pData == NULL) 
    { 
        m_nLength = 0; 
        return FALSE; 
    } 
    m_nLength = size; 
    return TRUE; 
} 
 
void CFileCache::ClearData() 
{ 
    if(m_pData != NULL) 
    { 
        ::HeapFree(::GetProcessHeap(), 0, m_pData); 
        m_pData = NULL; 
    } 
    m_nLength = 0; 
} 
 
// ファイルの読み込み 
BOOL CFileCache::Load(LPCWSTR filePath) 
{ 
    // 既存のメモリクリア 
    ClearData(); 
    // ファイルのオープン 
    HANDLE handle = ::CreateFileW( 
        filePath,      //ファイル名  
        GENERIC_READ,  //読み込み 
        0,             //排他的アクセス(共有無し) 
        NULL,          //セキュリティ情報 
        OPEN_EXISTING, //既存ファイルを開く 
        FILE_ATTRIBUTE_NORMAL,  //ファイル属性  
        NULL                    //読み込み時のテンプレートファイル 
    ); 
    if(handle == INVALID_HANDLE_VALUE) 
        return FALSE; 
    //ファイルサイズの範囲確認(int型の範囲か) 
    LARGE_INTEGER size; 
    BOOL result = ::GetFileSizeEx(handle, &size); 
    if(!result || size.HighPart != 0 || 
        size.LowPart > (DWORD) 0x7fffffff) 
    { 
        ::CloseHandle(handle); 
        return FALSE; 
    } 
    //メモリ確保 
    if(!AllocData((int)size.LowPart) ) 
    { 
        ::CloseHandle(handle); 
        return FALSE; 
    } 
    //読み込み 
    DWORD resultLength; 
    result = ::ReadFile( 
        handle, (LPVOID)m_pData, (DWORD)m_nLength, &resultLength, NULL); 
    ::CloseHandle(handle); 
    if(!result) 
    { 
        ClearData(); 
        return FALSE; 
    } 
    //読み込みサイズに修正 
    m_nLength = (int)resultLength;  
    //読み込み成功 
    return TRUE; 
} 
 
// インデックスを用いたアクセス 
char CFileCache::operator[](int ndx) 
{ 
    if(ndx <= 0 || ndx >= m_nLength) 
        throw (int)FILECACHE_OUTOF_RANGE; 
    return m_pData[ndx]; 
}

ここで、このクラス (CFileCache クラス) の機能を簡単に確認しておきます。

このクラスでは、ファイル全体をメモリ上に読み込み、インデックスを使用して、バイト単位でランダムに参照できるように実装されています。C++ を用いて、このクラスのオブジェクトにアクセスする際には、次のようなコードを用います。

例 9.2 (参考) CFileCache オブジェクトを利用する C++ のサンプル コード

C++
スクリプトの編集|Remove
#include "stdafx.h" 
#include "FileCache.h" 
 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
    CFileCache* pFileCache = new CFileCache();  //←[1] 
    BOOL b = pFileCache->Load(L"Test.txt");     //←[2] 
 
    try 
    { 
        ::printf("Length=%d\n", pFileCache->GetLength());  //←[3] 
        ::printf("%c\n", (*pFileCache)[2]);                //←[4] 
        ::printf("%c\n", (*pFileCache)[5]); 
    } 
    catch(int code)  //←[5] 
    { 
        ::printf("error=%d\n", code); 
    } 
 
    delete pFileCache;  //←[6] 
 
    return 0; 
}

[1] でオブジェクトを作成したのち、[2] のように Load メンバー関数を呼び出して、特定のファイルを読み込むと、オブジェクト内部ではファイル サイズ分のメモリが確保され、ファイル全体がメモリ内にキャッシュされます。

そして、[3] のように GetLength() 関数を呼び出すと、読み込んだファイルのバイト数が分かるほか、[4] のように "オブジェクト [インデックス]" という形式で、メモリ上にキャッシュされたデータに対して、バイト単位でランダムにアクセスできます (今回は簡単にするため読み取り専用)。このため、前述の CFileCache クラスでは、演算子 [ ] をオーバーロードしています。

また、インデックスでアクセスした際に、インデックスが範囲外の場合は例外が発生するので、[5] の catch ブロックで捕捉しています。

最後に、[6] でオブジェクトを削除することで、オブジェクト内部のキャッシュも削除されます。

今回作成するサンプルでは、例 9.2 と同様の方法で .NET 対応プログラムからもこの CFileCache クラスを利用できるようにするため、C++/CLI を用いて実装を追加していきます。

まずは、基本的な実装方法のポイントから確認していきましょう。

ページのトップへ


3. .NET 向けの基本的な実装 ~単純なラッパーではない点に注意~

例 9.1 のネイティブ コード版の CFileCache クラスを .NET 環境から利用させるための、最も基本的な実装方法は、C++/CLI を用いてマネージ コードのクラスを作成し、このマネージ クラスを介して、ネイティブ コードのクラスを利用させる方法です。つまり、ネイティブ コード版の CFileCache オブジェクトにアクセスするための、マネージ コード版のラッパー クラスを作ります。

この方法では、ネイティブ コードのクラスに Load というメンバー関数があるのなら、ラップするマネージ クラスにも、Load メンバー関数 (メソッド) を作成し、そのマネージ コード版の Load メンバー関数を経由して、ネイティブ コード版の Load メンバー関数を呼び出します。たとえば、前述の CFileCache クラスのラッパーを FileCacheWrapper クラスとして実装した場合、最終的には、C# から利用する場合、次のようなコードが考えられます。

例 9.3 (参考) CFileCache クラスのラッパーを利用する C# のサンプル コード

C#
スクリプトの編集|Remove
using System; 
using MyInteropLib; 
 
namespace CSConApp 
{ 
    class Program 
    { 
        static void Main() 
        { 
            FileCacheWrapper fileCache = new FileCacheWrapper(); //←[1] 
            bool result = fileCache.Load("Test.txt");            //←[2] 
 
            try 
            { 
                Console.WriteLine("Length={0}", fileCache.Length);  //←[3] 
                Console.WriteLine("{0}", (char) fileCache[2]);      //←[4] 
                Console.WriteLine("{0}", (char) fileCache[5]); 
            } 
            catch (Exception ex)  //←[5] 
            { 
                Console.WriteLine(ex.GetType()); 
            } 
            fileCache.Dispose();  //←[6] 
        } 
    } 
}

この例 9.3 の C#のサンプル コードに付けられた番号は、例 9.2 の C++ のサンプル コードに付けられた各番号に対応しています。C# の構文についての説明は割愛しますが、C++ をご存じの方であれば、このサンプルが行っていることは想像が付くと思います。

C# の場合も、基本的にはラッパー クラスを介して、例 9.2 の C++ の場合と同様の操作を行えます。

しかし、単にラップするだけなら簡単なのですが、ネイティブ コードのオブジェクトと、.NET 対応のマネージ オブジェクトとでは、後処理などの振る舞いに違いがあるため、注意すべき点があります。

たとえば .NET の管理下にあるマネージ ヒープに確保されたオブジェクトであれば、明示的に削除するなどの後処理を行わなくとも、ガベージ コレクションなどによって、自動的に削除されます。これに対して、ネイティブ コードのオブジェクトでは、ヒープに確保したオブジェクトに対して、プログラマーが責任を持って削除する必要があります。つまり、オブジェクトの作成から削除に至るオブジェクトの管理方法やライフタイムに若干の違いがあり、この差異を考慮した実装が必要になるのです。

このあとは、この差異を考慮した実装も含め、このようなラッパー クラスを実装し、その特徴をいくつか確認していきます。

Note:

この後のサンプルでは、Visual C++ 2010 Express を用いながら、順に作成していきます。また、作成したサンプルの動作を確認するには、Visual C# 2010 Express などの他の言語製品も必要になりますが、机上でも確認ができるように、C# での実行結果なども示しています。

ページのトップへ


4. ラッパーのためのハイブリッド型のプロジェクトの準備

前述のようなラッパークラスを実装するには、C++/CLI を用いてマネージ コード対応のクラス本体を実装するほか、このラッパー クラスの内部ではネイティブ コードのクラスにアクセスするために、ネイティブ コードのプログラムも併記することになります。この理由から、最初にマネージ コードとネイティブ コードを併用する「ハイブリッド型」に対応したプロジェクトを用意する必要があります。

ネイティブ コードのクラス本体の実装とラッパー クラスの実装とを、必ずしも 1 つのプロジェクトにまとめる必要はありませんが、ここでは簡単にするため、ネイティブ コードのクラス本体のソース コードと、ラッパー クラスのソース コードを 1 つのプロジェクトにまとめることにします。

まずは、次の手順でプロジェクトを作成し、プロジェクト内にネイティブ コードの CFileCache クラス (例 9.1) を追加します。

  1. 「クラス ライブラリ」という名前のプロジェクト テンプレートを使用して、プロジェクトを新規作成します。プロジェクト名は「MyInteropLib」と指定し、ソリューション名は「MyInteropLibSol」と指定します。プロジェクトの作成場所は任意です。
  2. MyInteropLib プロジェクトが作成されたら、プロジェクト内の以下の 2 つのファイル (Class1 クラスの定義) は不要なので削除します。
    • MyInteropLib.h (削除)
    • MyInteropLib.cpp (削除)
  3. 例 9.1 のネイティブ コード版 CFileCache クラスを記述した 2 つのファイルを、以下に挙げる名前のまま、この MyInteropLib プロジェクトに追加します。
    • CFileCache.h (追加)
    • CFileCache.cpp (追加)
  4. 追加した CFileCache クラスは Windows API を使用しているため、Stdafx.h ヘッダー ファイルに、以下のように #include ディレクティブを追加します。

    例 9.4 Windows API を利用可能なようにヘッダーを修正

    ファイル名: Stdafx.h

    C++
    スクリプトの編集|Remove
    #pragma once 
     
    #include <Windows.h>
  5. ビルドを行い、正常終了することを確認します。

これで、このあと必要となる「ハイブリッド型」のプロジェクトの準備ができました。このクラス ライブラリ プロジェクトの主な目的は、.NET 対応のクラス ライブラリ (DLL) を作成することですが、プロジェクトのプロパティ ページで、「共通言語ランタイム サポート」の設定を確認すると、次図のように「共通言語ランタイム サポート (/clr)」となっています。前回触れたように、これは「ハイブリッド型」のアプリケーションを構築するための設定です。

図 9.2

図 9.2 マネージコードとネイティブコードを併用可能な設定

ページのトップへ


5. ラッパー クラスの作成

それでは、ネイティブ コード版の CFileCache クラスをラッブした、マネージ コード版のクラスを作成してみましょう。ここでは、FileCacheWrapper という名前のクラスを作成します。

まずは、次の手順でクラスを追加しましょう。

  1. ソリューション エスクプローラーのツリー上で、MyInteropLib プロジェクト ノードを右クリックし、ショートカット メニューが表示されたら、[追加]、[クラス] の順にクリックします。
  2. [クラスの追加] ダイアログ ボックスが表示されたら、一覧から「C++ クラス」を選択して、追加をクリックします。
  3. 次図のように、「汎用 C++ クラス ウィザード」が起動するので、右側中央の[マネージ]チェック ボックスがチェックされていることを確認します。クラス名欄には「FileCacheWrapper」と入力し、ファイル名は既定のままにして、[完了] ボタンをクリックします。

図 9.3

図 9.3 マネージ クラスの追加

[マネージ] チェック ボックスをチェックすることで、マネージ コード対応クラス (マネージ クラス) のソース コードのひな形が生成されます。

FileCacheWrapper クラスのひな形が生成されたら、CFileCache クラスのラッパー クラスとするため、次のように修正してください。(個々の意味については、順に説明します。)

例 9.5 FileCacheWrapper クラス

ファイル名: FileCacheWrapper.h

C++
スクリプトの編集|Remove
#pragma once 
 
#include "FileCache.h" 
 
using namespace System; 
 
namespace MyInteropLib      //←[1] 
{ 
    ref class ManagedData;  //←[2] 
 
    public ref class FileCacheWrapper  //←[3] 
    { 
    private: 
        CFileCache* m_pCache;   //←[4] 
        ManagedData^ m_MData;   //←[5] 
    public: 
        FileCacheWrapper();     //←[6] 
        ~FileCacheWrapper();    //←[7] 
        !FileCacheWrapper();    //←[8] 
    public: 
        bool Load(String ^filePath);            //←[9] 
        property unsigned char default[int]     //←[10] 
        { 
            unsigned char get(int ndx)  //←[11] 
            { 
                if(ndx < 0 || ndx >= m_pCache->GetLength())  //←[12] 
                    throw gcnew IndexOutOfRangeException();  //←[13] 
                return (*m_pCache)[ndx];                     //←[14] 
            } 
        } 
        property int Length  //←[15] 
        { 
            int get() { return m_pCache->GetLength(); } 
        } 
    }; 
 
    ref class ManagedData  //←[16] 
    { 
    public: 
        ManagedData() {} 
    public: 
        ~ManagedData()  
        { 
            this->!ManagedData(); 
        } 
        !ManagedData() 
        { 
        } 
    }; 
}

ファイル名: FileCacheWrapper.cpp

C++
スクリプトの編集|Remove
#include "StdAfx.h" 
#include "FileCacheWrapper.h" 
 
using namespace System; 
using namespace System::Runtime::InteropServices;  
 
using namespace MyInteropLib; 
 
FileCacheWrapper::FileCacheWrapper()  //←[17] 
{ 
    m_pCache = new CFileCache();      //←[18] 
    m_MData  = gcnew ManagedData();   //←[19] 
} 
 
bool FileCacheWrapper::Load(String^ filePath)  //←[20] 
{ 
    IntPtr p1; 
    p1 = Marshal::StringToHGlobalUni(filePath); 
    BOOL b = m_pCache->Load((LPCWSTR)p1.ToPointer());  //←[21] 
    Marshal::FreeHGlobal(p1); 
    return (b) ? true : false; 
} 
 
FileCacheWrapper::~FileCacheWrapper()  //←[21] 
{ 
    delete m_MData;                    //←[22] 
    this->!FileCacheWrapper();         //←[23] 
} 
 
FileCacheWrapper::!FileCacheWrapper()  //←[24] 
{ 
    delete m_pCache;                   //←[25] 
}

まず、全般的な構成について確認します。

ラッパー クラス FileCacheWrapper は、[3] に定義されており、前回触れたように「ref class」と記述されているので、これは参照型のクラスです。

このクラス内部では、[4] に CFileCache オブジェクトのポインター変数 m_pFileCache を用意しており、必要に応じて、このポインターを使用して、CFileCache オブジェクトにアクセスします。たとえば、[20] の Load メソッドが呼び出されると、[21] のようにポインター変数 m_pFileCache を介して、同名の Load メンバー関数を呼び出しています。ラッパー クラスの他のメンバーも、基本的には同様の方法で、ネイティブ版のオブジェクトにアクセスします。

また、この FileCacheWrapper クラスでは、[5] のように、マネージ クラス ManagedData のオブジェクトもメンバーとして追加しました。この ManagedData の定義は、[16] にあります。

結局のところ、ラッパー クラスである FileCacheWrapper は、そのメンバーとして (いわば子オブジェクトとして)、[4] の CFileCache 型のネイティブ オブジェクトと、[5] の ManagedData 型のマネージ オブジェクトという 2 種類のオブジェクトを持っていることになります。このうち、後者の ManagedData オブジェクト自体は特別な機能を実装していませんが、特に後処理において、これら 2 種類のオブジェクトの扱いに注意すべき点があるので、このような構成にしました。

このあとは、このサンプル コードの中で、いくつか特徴と注意点を確認してみましょう。

ページのトップへ


6. ラッパー クラスの公開 ~アクセス修飾子の注意点~

この例 9.5 の C++/CLI を用いたサンプル コードでも、private や public などのアクセス修飾子が登場し、その使い方は従来の C++ とほぼ同様です。ただし、従来の C++ におけるアクセス修飾子と若干の違いがあります。

ネイティブ コードにおいる C++ のアクセス修飾子は、ソース コードにおいて、型 (クラス) の外からアクセスできるか否かなどの制御は可能ですが、プログラム ファイルの外部からアクセスできるか否かを制御する効力はありません。たとえば、DLL ファイルに C++ のクラスを実装し、そのクラスを DLL ファイルの外部から利用できるように公開するには、クラスをエクスポートするための専用の記述が必要になります。

一方、.NET のマネージ コードでは、public であることは、型 (クラス) の外部からアクセスできるだけでなく、DLL ファイル (正確にはアセンブリ ファイルという) の外部からアクセス可能であることを意味しています。エクスポートなどの特殊な記述は必要ありません。

ここでは、[3] の FileCacheWrapper クラスは、外部のプログラムから利用されるので public 修飾子が付いています。一方、[16] の ManagedData クラスは DLL ファイル (アセンブリ ファイル) の外部に公開する必要はないので、public が付いていません。その結果的、アセンブリの内部だけで利用できるようになります。

なお、名前空間の使用方法も従来と同様であり、今回は [1] の MyInteropLib 名前空間の中に、各クラスが定義されています。名前空間には public などのアクセス修飾子を付けることはできませんが、複数のクラスの論理的なグループとして、DLL ファイルの外部からも認識できます。

ページのトップへ


7. 各メンバーの定義

次に、この FileCacheWrapper クラスの機能を確認しながら、クラス内の各メンバーの特徴について確認します。

既に触れたように、この FileCacheWrapper クラスは、CFileCache オブジェクト (インスタンス) のラッパー クラスです。そのため、FileCacheWrapper クラスのインスタンスが存在している間は、内部的に CFileCache インスタンスが存在している必要があります。よって、FileCacheWrapper クラスのコンストラクターが実行される中で、CFileCache クラスのインスタンスも作成しています。

マネージ クラスのコンストラクターの定義も、従来の C++ と同様であり、この例では [6] および [17] にコンストラクターが定義されています。このコンストラクターでは、[18] でネイティブ オブジェクトの CFileCache インスタンスを作成し、[19] でマネージ オブジェクトの ManagedData インスタンスを作成しています。

これに対し、オブジェクトを破棄する際に行う後処理、つまりデストラクターでは、従来のそれとは少し異なります。C++/CLI の後処理を実装するメソッドには、「デストラクター」と「ファイナライザー」の 2 種類があります。デストラクターのメソッド名は従来と同様であり、[7] および [21] のとおり、クラス名の頭に「~」(チルダ) を付けます。一方、ファイナライザーは [8] および [24] にあるように、クラス名の頭に「!」(感嘆符) を付けます。この 2 つの後処理には使い分けがあり、のちほど改めて取り上げます。

メソッドの定義も、修飾子の使い方にいくつか違いがありますが、従来のメンバー関数と基本的な記述方法は同様です。ただし、今回のようなラッパー クラスでは、メソッドが受け取る引数はマネージ コードのデータ型ですが、さらに内部でネイティブ コードのメンバー関数を呼び出すので、引数をマネージ型からネイティブ型への変換 (またはその逆) を行う必要があります。[20] の Load メソッドでも、前回説明した Marshall クラスを用いて、マネージ対応の String 型の文字列と、LPCWSTR 型のポインターで表されるネイティブ対応との文字列で相互に変換を行っています。

また、[15] には Length プロパティが定義されています。プロパティは利用者側からすると、変数のように見えて、その実態は値の設定や参照を行うメソッドです。この例の Length プロパティの場合、例 9.3 の [3] の「fileCache.Length」とあるように、変数 Length のように参照できます。この参照を行うと、実際には、例 9.5 の [15] のブロック内にある get のブロック (get アクセサー) が実行され、その戻り値が返ります。

この例には記述してありませんが、値を設定するには set アクセサーを使用します。仮に、FileCacheWrapper クラスに Position というプロパティがあり、参照と設定が可能であるなら、get アクセサーset アクセサーは次のように定義できます。

例 9.6  (参考) Position プロパティの定義

C++
スクリプトの編集|Remove
public: 
    property int Position 
    { 
        int get() { return m_nPos; }              //←[1] 
        void set(int value) { m_nPos = value; }   //←[2] 
    }

このとき利用する側では、次のようにアクセスできます。

例 9.7 (参考) Position プロパティの利用

C++
スクリプトの編集|Remove
    fileCache->Position = 100;  //←[3] setアクセサーの呼び出し 
    fileCache->Position++;      //←[4] getアクセサーとsetアクセサーの呼び出し

例 9.7 の [3] のようにプロパティに値 100 を設定すれば、例 9.6 の [2] の set アクセサーが呼び出され、その引数 value には、100 が渡ります。通常は、この値の有効性を検証したのち、プライベート変数などに退避します。

少し注意すべき点は、例 9.7 の [4] のインクリメント演算子を使用した場合です。C++ をご存じの方は、かえって誤解される場合があり、[4] のように記述すると、「Position プロパティの get アクセサーが返す戻り値に対して単にインクリメントするだけなので、オブジェクト自身の Length プロパティ (内部的なプライベート変数の値) が増加しないのではないか」と疑問に思われることがあります。

しかし、C++/CLI のコンパイラーは [4] のような記述を見つけると、Position プロパティの get アクセサーを呼び出して値を取得して加算したのち、今度は set アクセサーを呼び出して加算結果を設定するようにプログラム コードを生成します。つまり、論理的な Length という名前のデータに対して、直接インクリメントするかのような振る舞いになるのです。(これは、C# でも同様です。)

また、プロパティにはインデックスを付けることができます。たとえば、Lines という名前のプロパティがあり (オブジェクトの変数名を obj とした場合) 、このプロパティに対して、obj.Lines[0]、obj.Lines[1]、...、obj.Lines[2]というように、インデックスを付けてアクセスできます。このときのプロパティの定義は次のようになります。

例 9.8 (参考) インデックス付きの Lines プロパティ

C++
スクリプトの編集|Remove
public: 
    property int Lines[int] 
    { 
        int get(int ndx) { /* getアクセサーの実装 */ } 
        void set(int ndx, int value) { /* setアクセサーの実装 */ } 
    }

この例のように、get アクセサーや set アクセサーの引数が 1 つ増えました。引数 ndx には、呼び出し元が指定したインデックの値が渡ってきます。

さらに、このインデックス付きプロパティでは、プロパティ名自体を省略して、オブジェクト変数の後ろに、直接インデックスを付けるように定義できます。たとえば、obj[0]、obj[1]、...、obj[2] のように記述できます。このように記述すると、1 つのオブジェクトをあたかも配列やコレクションのように利用できます。このためには、例 9.8 の Lines プロパティを定義する際に、「Lines」という名前の箇所に対して、名前の代わりに「default」キーワードを記述します。

このような名前を省略できるインデックス付きプロパティは、例 9.5 の FileCacheWrapper クラスの [10] に定義してあります。このプロパティを利用した例が、例 9.3 の [4] にある「fileCache[2]」という記述です。

また、例 9.5 の [10] のインデックス付きプロパティは、例 9.1 のネイティブ版の CFileCache クラスに定義された演算子 [ ] のオーバーロード (例 9.1 の末尾の CFileCache::operator[](int ndx)) に相当するものであり、ここでは、これをラップしたものが例 9.5 の [10] のプロパティに当たります。

なお、このプロパティの中の [11] の get アクセサーでは、指定されたインデックスが有効範囲であるか、あらかじめ確認するために利用できます。ここでは [12] のように、有効な範囲であるかチェックし、有効でない場合は、[13] のように例外をスローしています。従来の C++ とは異なり、スローできるデータは、マネージ クラスである Exception クラスか、または、その派生クラスだけです。ここでは、インデックスの範囲外を意味する「IndexOutOfRangeException」をスローしています。このクラスも、Exception クラスの派生クラスです。

ページのトップへ


8. 後処理の注意点 ~デストラクターとファイナライザー~

ここで、マネージ オブジェクトの後処理を担当する 2 つのメソッド「デストラクター」と「ファイナライザー」の使用方法について改めて確認します。

.NET では、gcnew キーワードを用いてマネージ ヒープに確保したオブジェクトについては、プログラム内のどこからも参照されなくなると、ガベージ コレクションによって自動的に削除されます。このガベージ コレクションによってオブジェクトが削除される際に、後処理として自動的に呼び出されるのが「ファイナライザー」です (例 9.5 の [8]、および [24])。

このファイナライザーは、リソースの解放などの後処理を行うべき場所ですが、ガベージ コレクションがいつ起こるかは特定できないので、不必要にリソースの解放が遅れる場合があります。リソースによっては、必要なくなったタイミングで、速やかにプログラマーが明示的に解放したほうがよい場合もあります。そのような明示的な後処理を行うのが「デストラクター」です (例 9.5 の [7]、および、[21])。

このマネージ クラスのデストラクターを呼び出すには、C++/CLI の場合は、従来どおり、delete キーワードを用いて、次のように記述できます。

例 9.9 C++/CLI における明示的なデストラクターの呼び出し

C++
スクリプトの編集|Remove
delete pFileCache;

ただし、この記述はデストラクターを呼び出して、オブジェクトが持つリソースの解放など、あくまで後処理を行うためのものです。オブジェクト自身の削除は行いません。オブジェクト自身は、ガベージ コレクションによって削除されるので、この delete キーワードによる記述によって、オブジェクト自体が削除されるわけではないのです。

また、デストラクターとファイナライザーにおける後処理には、それぞれ行うべき処理に作法があり、まとめると次のようになります。

このあとは、これらを行う理由や記述方法について、それぞれ個別に確認しましょう。

ページのトップへ


9. デストラクターで実装すべきこと

前述のおとり、デストラクターの作法として (a) から (c) までの 3 つの行うべきことがあります。
これを例 9.5 の [21] のデストラクターに当てはめてみましょう。

この [21] のデストラクターは、基本的にはコンストラクターの中で作成した子オブジェクトに関して、後処理を行う必要があります。この例のコンストラクター [17] では、子オブジェクトとしてマネージ オブジェクト (m_MData) とネイティブ オブジェクト (m_pCache) を作成しいるので、[21] のデストラクターでは、この 2 つのオブジェクトに関して後処理を行う必要があります。

それが、作法の (a) と (b) に当たる部分であり、それぞれ [22] と [23] になります。[22] では、マネージ オブジェクトである m_MData に対して delete キーワードを用いて、明示的に後処理を行っています。また、[23] はネイティブ オブジェクトの後処理を行うもので、「delete m_pCache」となるべき箇所ですが、この記述がちょうど [24] のファイナライザーに実装されているので、[23] からファイナライザーを呼び出して、ネイティブ オブジェクトの後処理を行っています (もちろん、ネイティブ オブジェクトは、delete キーワードを使用することによって、オブジェクト自身も削除されます)。

ここまでは、コンストラクターの逆を行えばよいので、特に問題はないでしょう。留意すべき点は、 (c) のファイライザー呼び出しの抑止です。

デストラクターもファイライザーも、当該オブジェクトの後処理を実行するためのものなので、デストラクターを明示的に呼び出したのであれば、ガベージ コレクションの際に、ファイナライザーをわざわざ呼び出す必要はありません。

.NET Framework の観点からすると、ファイナライザーとは Object クラスの Finalize メソッド (および、派生クラスでオーバーライドした同名メソッド) のことであり、このファイナライザーの呼び出しを抑止するために、.NET Framework のクラス ライブラリの GC クラスには、SuppressFinalize メソッドが用意されています。このメソッドの引数に、当該オブジェクトを渡して呼び出すと、ガベージ コレクションの際にファイナライザーを呼び出さないように、抑止することができます。

このようなファイナライザーの呼び出しを抑止することで、ガベージ コレクションでのオーバーヘッドも軽減することができます。実は、ファイナライザーを呼び出す場合と、呼び出さない場合とでは、ガベージ コレクションにおける後処理の工程が異なるのです。

ファイナライザーを呼び出す必要がないオブジェクトの場合、定期的に行われるガベージ コレクションの過程で、不要なオブジェクトであると認識されると、その 1 回のガベージ コレクションによって、オブジェクトは削除されます。

ところが、ファイナライザーの呼び出しを必要とするオブジェクトの場合は、1 回のガベージ コレクションでは削除されません。まず、1 回目のガベージ コレクションの際に、不要なオブジェクトであることが認識されると、この場では削除せずに、ファイナライザーを呼び出すための終了キューに登録します。この後、終了キューのオブジェクトは、まとめてファイナライザーが呼び出されます。そして、2 回目のガベージ コレクションの際に、ファイナライザーの呼び出し済みオブジェクトを削除します。

つまり、ファイナライザーを呼び出さない場合には、1 回のガベージ コレクションで削除されますが、ファイナライザーを呼び出す場合には、2 回のガベージコレクションが必要になります。このため、呼び出す必要がなければ抑止するのが望ましく、また、なまじっか空のファイナライザー (Finalize メソッド) を記述するくらいなら、何も書かないほうがオーバーヘッドを少なくできるのです。

しかし、C++/CLI の場合は、このような GC.SuppressFinalize メソッドの明示的な呼び出しは不要です。実は、デストラクターを記述すると、コンパイラーによってデストラクターの末尾に、SuppressFinalize メソッド呼び出しが追加されるのです。そのため、.NET の後処理の作法としては、前述の (a) から (c) までの 3 つの実装が必要ですが、プログラマーが C++/CLI でデストラクターを記述する際には、(a) と (b) の後処理を記述します。

なお、C++/CLI のデストラクターは、コンパイラーによって、.NET の IDisposable インターフェイスの Dispose メソッドとして出力されます。つまり、Visual Basic や C# において、C++/CLI のデストラクターを呼び出す場合には、この Dispose メソッドを呼び出すことになります。このため、今回の FileCacheWrapper クラスのオブジェクトを C# から利用する場合は、例 9.3 の [6] でも後処理の際に、Dispose メソッドを呼び出しています。

Note:

正確にいうと、C++/CLI で記述したデストラクターのメソッド本体は、プライベートなメソッドになり、このプライベート メソッドを間接的に呼び出すパブリック メソッドとして、IDisposalbe インターフェイスの Dispose メソッドが、コンパイラによって生成されます (実際は、もうワンクッション別のメソッド呼び出しが中間に介在します) 。なお、この Dispose メソッドに関しては、ソース コード上のデストラクターのアクセス修飾子に関係なく、パブリック メソッドになります。

ページのトップへ


10. ファイナライザーで実装すべきこと

ファイナライザーに後処理を実装しておけば、万が一、プログラマーがデストラクターの呼び出しを忘れても、ガベージ コレクションの際には、確実に後処理を行うことができます。ただし、行うべき後処理に注意してください。前述の作法 (d) に挙げたとおり、ネイティブ オブジェクトの後処理だけです。

作法の (a) に挙げたような、子オブジェクトとして含むマネージ オブジェクトの後処理は必要ありません。子オブジェクトの後処理としては、やはり、ガベージ コレクションによって自動的にファイナライザーが呼び出されるので、プログラマーが呼び出す必要はないのです。

それに呼び出すことが不適切の場合もあります。というのは、親オブジェクトのファイナライザーと子オブジェクトのファイナライザーとの呼び出し順は不定であり、親オブジェクトのファイナライザーが呼び出される前に、子オブジェクトのファイナライザーが先に呼び出され、子オブジェクトの後処理が既に完了している場合もあるからです。

例 9.5 の [24] のファイナライザーでも、[25] の「delete m_pCache;」のようにネイティブ オブジェクトの後処理しか行っていません。

なお、[24] のファイナライザーが実行される時点では、メンバー変数 m_MData が子オブジェクトである ManagedData を参照してするため、「親オブジェクトよりも先に子オブジェクトが削除されることがないのでは」と疑問に思うかもしれません。しかし、親オブジェクトが参照していたとしても、この時点では、子オブジェクトの後処理が完了している可能性があるのです。

この理由は、ガベージ コレクションの工程において、オブジェクトが参照されているか否かの判断を行う際に、単純に変数がオブジェクトを参照しているか否かをチェックするのではなく、現在実行しているプログラム内で有効な変数から参照できるかを確認するからです。

たとえば、現在使用できるローカル変数やメンバー変数、そのほかの有効な共有変数などから、参照可能なオブジェクトを突き止め、さらに、このような参照可能なオブジェクトの変数から、参照できるオブジェクトを追跡し、芋づる式に、参照可能なオブジェクトであるかを確認します。このため、この参照の連鎖をたどって到達できない親オブジェクトがある場合、その親が排他的に参照する子オブジェクトにもたどりつけません。よって、そのような親オブジェクトが、参照している子オブジェクトも、結局のところ、参照できないオブジェクトとして扱われます。こうして、親オブジェクトと子オブジェクトは、同時に参照できないオブジェクトとして認識できるのです。

ここで、後処理の実装方法をまとめておきましょう。

結局のところ、C++/CLI における後処理の作法としてコードに明記するのは、(a) 、 (b)、および (d) です。この基本パターンを表したのが、例 9.5 の [21] のデストラクターと [24] のファイナライザーです。特に、デストラクター内の作法 (b) のネイティブ オブジェクトの後処理に関しては、例 9.5 の [23] のように、ファイナライザーを呼び出すことで実現できます。

まずは、この例の基本パターンを覚えておくとよいでしょう。

なお、今回の例ではマネージ クラスの構造を単純にして分かりやすくするため、マネージ クラスから継承した派生マネージ クラスでの、デストラクターやファイナライザーの例は取り上げませんでした。基本的には、派生クラスのデストラクターやファイナライザーでは、自身のメンバーの後処理に関して実装すれば十分です。C++/CLI のコンパイラーは、派生クラスの後処理の工程において、基本クラスのデストラクターやファイナライザーに実装された後処理を呼び出すように、コードを自動生成してくれます。

Note:

言語によっては、デストラクターやファイナライザーの名称などが異なりますが、デストラクター (Dispose メソッド) やファイナライザー (Finalize メソッド) に関する前述の (a) から (d) の作法は、Visual Basic や C# でも当てはまります。Visual Basic や C# の後処理の実装例は、以下でも紹介されています。

ページのトップへ


11. サンプルの利用

最後に参考までに、このラッパー クラスを C# から利用した実行の様子を示しておきます。

前述のサンプルをビルドすると、正常終了すれば、以下の DLL ファイル (アセンブリ ファイル) が出力されます。

MyInteropLib.dll

この DLL ファイルへの参照の追加を行った C# のコンソール プロジェクトにおいて、例 9.3 のサンプル コードを入力して実行すると、次のようにコンソールには表示されます。(次図の実行結果では、プログラム ファイルと同一のフォルダーに「Test.txt」というファイルがあることが前提です。このファイルには「Test」という半角 4 バイトが、ANSI 文字列として含まれています。)

図 9.4

図 9.4 FileCacheWrapper を利用した実行結果

例 9.3 の [3] では、Length プロパティを参照した結果を表示するためのもので、実際には、図 9.4 のコンソールの 1 行目のように「Length=4」と表示されます。

また、例 9.3 の [4] では「fileCache[2]」とインデックスを使用して、3 バイト目を参照しています (先頭のインデックス値はゼロ)。ここでは、ファイルのデータは「Test」という 4 バイトになっており、図 9.4 のコンソールの 2 行目には「s」と表示されます。また、例 9.3 の [4] の次行では、「fileCache[5]」と参照しており、添え字が範囲外なので、例外が発生します。そのため、例 9.3 の [5] の catch ブロックに制御が移り、図 9.4 のコンソール画面には例外オブジェクトの型 (IndexOutOfRangeException) が表示されます。

ページのトップへ


12. まとめ

今回は、C++/CLI を使用して、既存のネイティブ コード版の C++ のクラスを、.NET 側から再利用できるようにするため、マネージ版ラッパー クラスの実装方法について確認しました。いくつか、主な C++/CLI の構文の特徴を確認したほか、マネージ クラスの中に、ネイティブ オブジェクトをラップするという実装形態において、留意すべき点を取り上げました。特に、.NET 管理下のマネージ オブジェクトと、ネイティブ環境下のオブジェクトでは、その削除方法が異なります。この違いを意識しながら、デストラクターやファイナライザーなどの後処理で実装すべき点などを確認しました。

ページのトップへ


Code Recipe Visual C++ デベロッパー センター

ページのトップへ