WisdomSoft - for your serial experiences.

ハンドル型

参照クラスのインスタンスを gcnew 演算子によって生成すると、演算子はポインタではなくハンドルを返します。ハンドルとは共通言語ランタイムによって管理されているオブジェクトを表し、基本的な考え方はポインタと同じですが、ポインタのようなアドレスの演算は行えません。

オブジェクトのハンドル

ネイティブ C/C++ の new 演算子は、生成したオブジェクトのポインタを返しました。一方で、C++/CLI の gcnew 演算子が返すオブジェクトの参照は共通言語ランタイムによって管理されているインスタンスへのポインタなので、ネイティブ C/C++ のポインタのように自由に演算を行ってよいアドレスとは性質が異なります。

実質的に、参照型クラスのオブジェクトはポインタとして扱われるべきものではありません。C++/CLI では、共通言語ランタイムが管理しているインスタンスを参照する変数を表現する手段として、新しくハンドル型を用意しています。ハンドル型は、性質的にはポインタと類似していますが、C++ のポインタ型 * や参照型 & よりも安全にオブジェクトを参照することができます。

ハンドル型の変数を宣言するには、変数名の前に ^ 記号を指定します。例えば、参照クラス型 T のハンドル型変数 handle は次のように宣言します。

T ^ handle;

ハンドル型変数に代入可能なのは、参照クラス型オブジェクトへのハンドルです。gcnew 演算子が返す結果こそ、オブジェクトへのハンドルです。参照クラス型 T のインスタンスを生成し、変数に代入する場合は次のように記述できます。

T ^ handle = gcnew T;

gcnew 演算子は、共通言語ランタイムが管理しているマネージヒープと呼ばれる領域に配置します。実質的に、gcnew が返すハンドル型の値はマネージヒープに配置されているオブジェクトへの参照ですが、オブジェクトの物理的な配置を管理するのは共通言語ランタイムなので、実行時にアプリケーションがポインタとしてアドレス操作をすることは望まれません。よって、ハンドル型変数はオブジェクトへの参照を保存している単純な変数にすぎないのです。ネイティブ C/C++ 言語で使っていたトリッキーなポインタ操作はハンドル型で行うことはできません。ハンドル型変数が保存しているのは常に有効なオブジェクトへの参照か、または参照していないかです。

ハンドル型変数が参照しているオブジェクトの操作方法は、ポインタ型変数からオブジェクトを操作する従来の方法と同じです。メンバアクセス演算子 -> からオブジェクトのメンバにアクセスすることができます。例えば、ハンドル型変数 handle が参照するオブジェクトの Method() メソッドにアクセスする場合は次のように記述します。

handle->Method();

これは、標準 C++ 言語の操作方法と同じです。ハンドル型は -> を用いてハンドル型変数が指しているオブジェクトのメンバにアクセスしています。ハンドル型の変数からオブジェクトの実体を参照する方法は C++ 言語と同様に * 演算子を使って行うことができるため、上記の -> を使ったメンバアクセスは以下のように書き換えることも可能です。

(*handle).Method();

これは、handle->Method() と同義です。

コード1
public ref class Managed 
{
public:
	int field;
};

int main(int argc, char ** args)
{
	Managed ^ handle = gcnew Managed();
	handle->field = 10;

	System::Console::WriteLine(handle->field);

	return 0;
}
実行結果
コード1 実行結果

コード1は、ref キーワードを持つ Managed 参照クラスのインスタンスを gcnew 演算子で作成し、その結果をハンドル型の handle 変数に代入しています。その後、Managed クラスの field フィールドに 10 を代入し、WriteLine() メソッドで出力しています。変数がハンドル型であるという点を除けば、基本的なオブジェクトの操作方法は同じです。

ハンドル型変数は、マネージヒープに配置されているオブジェクトを参照している変数という意味に抽象化されているため、ポインタのように変数を数値化してメモリアドレスの演算を行うということはできません。しかし、オブジェクトを参照するための、オブジェクトの場所を保存しているという意味ではポインタと概念は同じです。

例えば、ハンドル型変数を同じハンドル型の変数に代入した場合はどのようになるでしょう。C/C++ でポインタの世界を理解していれば容易に想像できることでしょう。例え異なる変数にハンドルを代入しても、メモリ上のオブジェクトの実体が複製されるようなことはありません。オブジェクトの参照が複製され、複数の変数が 1 つのオブジェクトを参照するという結果になります。

コード2
public ref class Managed 
{
public:
	int field;
};

int main(int argc, char ** args)
{
	Managed ^ obj1 = gcnew Managed();
	obj1->field = 10;

	Managed ^ obj2 = obj1;
	obj2->field = 20;

	System::Console::WriteLine(obj1->field);

	return 0;
}
実行結果
コード2 実行結果

コード2は、Managed^ 型の変数 obj1 に gcnew でインスタンス化した Managed オブジェクトのハンドルを代入して初期化します。Managed クラスは field 整数型フィールドを公開しているため、obj1->field = 10 で field の値を 10 に設定します。 次に、obj1 と同じ Managed^ 型の変数 obj2 を宣言し、この変数に obj1 を代入します。このとき、obj1 変数に保存されているオブジェクトへの参照が複製されますが、メモリに配置されているオブジェクトの実体が複製されるわけではありません。

nullptr

ポインタでは、ポインタ型変数が適切なメモリオブジェクトを参照していない状態を表現する方法として、アドレス 0 番を使っていました。つまり、ポインタ型変数の値が 0 と等しければ、そのポインタは適切な参照を持たない NULL ということです。しかし、ポインタの場合は例え 0 以外の値でも、正しいメモリアドレスを参照しているかどうかは、実行時に参照しない限り判断することはできませんでした。そのため、C/C++ 言語のプログラムではしばしば不適切なメモリアドレスを参照することによる強制終了が発生します。

ハンドル型は、メモリアドレスを操作する変数ではないので 0 を NULL とすることはできません。ハンドル型の変数に 0 を NULL として代入することはできませんし、ハンドル型を int 型と比較することもできません。そこで、ハンドル型で適切なオブジェクトを参照していない状態を表す値に nullptr キーワードが使われます。nullptr キーワードは標準 C++ である ISO/IEC 14882:2011 通称 C++11 にも導入されましたが、C++/CLI との相互運用には問題ありません。

nullptr は、ハンドル型変数に代入すると、そのハンドル型変数が適切なオブジェクトを参照していないことを表すことができます。当然、ハンドル型変数と nullptr を == や != 演算子で比較することができ、if 文などでオブジェクトが初期化されているかどうかを調べる手段として利用することができます。

また、nullptr は通常のポインタ型変数に代入することも可能です。ポインタ型変数に代入した場合は 0 を代入する従来の NULL ポインタと同じ意味であると解釈されます。ポインタ型変数と nullptr を比較することも可能ですが、nullptr を 0 と比較することはできません。

コード3
#include <iostream>

public ref class Managed 
{
public:
	int field;
};

int main(int argc, char ** args)
{
	int * p = nullptr;
	Managed ^ handle = nullptr;
	//int i = nullptr; //エラー

	System::Console::WriteLine(p == nullptr);
	System::Console::WriteLine(p == 0);
	System::Console::WriteLine(handle == nullptr);
	System::Console::WriteLine(NULL.Equals(handle));
	//System::Console::WriteLine(nullptr == 0); //エラー

	return 0;
}
実行結果
コード3 実行結果

コード3は、nullptr リテラルでポインタ型変数 p と Managed へのハンドル型変数 handle を初期化しています。よって、これらの変数は適切なオブジェクトを参照していません。

ポインタ型変数も、ハンドル型変数も、共に nullptr と等しいかどうかを == や != で調べることができます。よって p == nullptr や handle == nullptr という式は適切です。p も handle も nullptr で初期化されているため、両方とも結果は true が返されるでしょう。また、ポインタ型変数は実質的にメモリアドレスを格納する整数型変数なので 0 と比較することに問題はありません。

では、0 を表す定数 NULL を Int32 オブジェクトとして Equals() メソッドで nullptr と比較すればどうなるでしょう。それを実験しているのが NULL.Equals(handle) という式です。この結果は false となります。数値オブジェクト 0 とオブジェクトを参照しないことを表す nullptr は異なります。