Kenny Kerr
Software Consultant
July 2004
日本語版最終更新日 2005 年 6 月 6 日
適用対象:
Microsoft Visual C++ 2005
Microsoft Visual C++ .NET
Common Language Runtime (CLR)
Microsoft Visual Studio 2005
要約:Visual C++ 2005 に導入される新しい C++/CLI 言語の設計および原理を説明します。こうした知識は、.NET プログラミングの最良な言語を使用して、強力な .NET アプリケーションを作成するのに役立ちます。
目次
はじめに
オブジェクトのコンストラクト
メモリ管理とリソース管理
メモリ管理
リソース管理
型についての再考
ボックス化
参照型および値型の作成
アクセシビリティ
プロパティ
デリゲート
まとめ
はじめに
Visual C++ 開発チームは、時間をかけてユーザーから意見を聞き、.NET と C++ に携わってきた結果、Visual C++ 2005 では、共通言語ランタイム (CLR) のサポートを設計し直すという判断に至りました。新しい設計は、C++/CLI と呼ばれ、CLR 型を使用、編集する、より自然な構文を提供するようになっています。この記事では、この新しい構文を、CLR を対象とするもっとも関連の深い 2 つの言語、現在の C# およびマネージ C++ と比較しながら説明します。必要に応じて、ネイティブ C++ の類似の概念についても説明します。
共通言語基盤 (CLI : Common Language Infrastructure) は、Microsoft .NET が基準とする仕様の集まりです。CLR は、CLI のマイクロソフトの実装です。C++/CLI 言語の設計は、C++ による CLI の自然なサポートを目的とし、Visual C++ 2005 コンパイラは、CLR 用の C++/CLI を実装しています。
Visual C++ 2005 コンパイラ、および C++/CLI 言語の設計を評価するにあたって、重要なメッセージが 2 つあります。1 つは、Visual C++ は、CLR を扱うもっともロー レベルなプログラミング言語という位置づけであるということです。他の言語を使用する必要はありません。Microsoft Intermediate Language (MSIL) を使用する必要もありません。2 つ目は、.NET プログラミングは、ネイティブ C++ プログラミングと同じように自然であるということです。この記事によって、この 2 つのメッセージが明確になるでしょう。
この記事は、C++ プログラマを対象としています。C# または Visual Basic .NET からの切り替えをお勧めしているわけではありません。C++ を既に好んで使用しており、C++ が従来から提供している機能はすべて活用したいが、それでも C# の生産性も欲しい、という方にこの記事が役立つでしょう。また、ここでは、CLR や .NET Framework の紹介は行いません。説明の中心は、Visual C++ 2005 によって、.NET Framework 対応のよりエレガントで効率的なコードをどのように作成できるか、ということです。
オブジェクトのコンストラクト
CLR は、2 つの型、値型と参照型を定義します。値型は、効率的に割り当て、アクセスできるように設計されています。値型は、C++ に組み込みの型と非常に似ており、ユーザーが独自に作成することもできます。これは、Bjarne Stroustrup が具体的な型と呼ぶものです。一方、参照型は、クラス階層と、それに関連するすべて (たとえば、派生クラス、仮想関数など) を作成する機能など、オブジェクト指向プログラミングとして期待されるすべての機能を提供するように設計されています。参照型は、ガベージ コレクションと呼ばれる自動メモリ管理など、ランタイムの追加機能も、CLR によって提供します。CLR は、参照型および値型両方の精密な実行時型情報の提供も行います。この機能は、リフレクションと呼ばれます。
値型は、スタックに割り当てられます。参照型は、マネージ ヒープに割り当てられます。このヒープは、CLR のガベージ コレクション (GC) が管理するヒープです。C++ でアセンブリを開発する場合、ネイティブ C++ 型を、これまで通り、CRT ヒープに割り当てることができます。今後、Visual C++ 開発チームでは、ネイティブ C++ 型もマネージ ヒープに割り当てられるようにする予定です。こうなると、ガベージ コレクションは、ネイティブ型にとっても同じように魅力的な手法となります。
ネイティブ C++ では、特定のオブジェクトをどこに作成するかを選択します。どの型も、スタック、あるいは CRT ヒープへの割り当てが可能です。
// スタックに割り当てる
std::wstring stackObject;
// CRT ヒープに割り当てる
std::wstring* heapObject = new std::wstring;
上記の通り、オブジェクトをどこに割り当てるかの選択は、その型に関係しないため、完全にプログラマの判断で決定します。また、スタック割り当てとヒープ割り当ての構文は異なります。
一方、C# では、値型はスタックに、参照型はヒープに作成します。次の例に示す、System.DateTime 型は、作成者が値型として宣言しています。
// スタックに割り当てる
System.DateTime stackObject = new System.DateTime(2003, 1, 18);
// マネージ ヒープに割り当てる
System.IO.MemoryStream heapObject = new System.IO.MemoryStream();
上記の通り、オブジェクトを宣言する方法は、そのオブジェクトをスタック、またはヒープのどちらに作成するかにまったく影響しません。どちらに作成するかは、型の作成者とランタイムが完全に決定します。
C++ マネージ拡張 (略して マネージ C++) は、マネージ コードとネイティブ C++ コードを混在させる機能を導入しました。この拡張は、C++ が、C++ 標準の規則に従いながらも、CLR コンストラクトを完全にサポートするように追加されました。しかし、拡張が多すぎ、C++ で多くのマネージ コードを記述することが深刻な問題となりました。
// スタックに割り当てる
DateTime stackObject(2003, 1, 18);
// マネージ ヒープに割り当てる
IO::MemoryStream __gc* heapObject = __gc new IO::MemoryStream;
C++ プログラマにとって、値型をスタックに割り当てることは、きわめて普通のことです。マネージ ヒープの例は、少し、奇妙に思えます。__gc は、マネージ C++ 拡張のキーワードの 1 つです。マネージ C++ は、この状況でのプログラマの意図を予測できるので、上記の例は、__gc キーワードを省略して記述できます。この記述が標準とされています。
// マネージ ヒープに割り当てる
IO::MemoryStream* heapObject = new IO::MemoryStream;
これは、ネイティブ C++ によく似ています。問題は、heapObject が本当の C++ ポインタでないことです。C++ プログラマは、値を固定するためにポインタをよく使用しますが、ガベージ コレククタは、いつでもメモリ内のそのオブジェクトを移動することができます。もう 1 つ不都合な点として、コードからは、オブジェクトがネイティブ ヒープまたは マネージ ヒープのどちらに割り当てられるかがわからないということが挙げられます。型が作成者によってどのように定義されているかを確認しなければなりません。他にも、C++ ポインタをオーバーロードすることが良くないと考えられる多くの理由があります。
C++/CLI は、CLR オブジェクト参照と、C++ ポインタとを区別するハンドルの考え方を導入しています。C++ ポインタをオーバーロードしないことで、言語のあいまいさの多くを解消しています。さらに、ハンドルによって CLR のより自然なサポートが提供されます。たとえば、C++ では直接、参照型にオーバーロードした演算子を使用できます。これは、ハンドルで演算子のオーバーロードがサポートされているためです。これは、"マネージ" ポインタでは不可能でした。C++ では、ポインタの演算子をオーバーロードすることが許されていないためです。
// スタックに割り当てる
DateTime stackObject(2003, 1, 18);
// マネージ ヒープに割り当てる
IO::MemoryStream^ heapObject = gcnew IO::MemoryStream;
繰り返しですが、値型の宣言に驚きはありません。しかし参照型は、独特です。^ 演算子は、CLR 参照型へのハンドルとして変数を宣言します。ハンドル追跡 (ハンドルの値を意味する) は、参照するオブジェクトがメモリ内を移動すると、ガベージ コレクタによって自動的に更新されます。さらに、ハンドルはバインドし直すことが可能なので、C++ ポインタのように、他のオブジェクトを示すことができます。さらに、new 演算子の代わりに使用する gcnew 演算子にも注意が必要です。この演算子は、オブジェクトがマネージ ヒープに割り当てられることを明確に示します。new 演算子は、マネージ型にオーバーロードされることはなくなり、新しい独自の演算子を提供していなければ、オブジェクトを CRT ヒープにのみ割り当てます。C++ はすばらしい!
これが、一言で言えば、オブジェクトのコンストラクトです。つまり、ネイティブ C++ ポインタは、CLR オブジェクト参照とは明らかに違います。
メモリ管理とリソース管理
ガベージ コレクタを含む環境を扱う場合は、メモリ管理とリソース管理を区別して考えることをお勧めします。通常、ガベージ コレクタは、オブジェクトを含むメモリの割り当てや開放に関係があります。オブジェクトが保持するその他のリソース、たとえば、データベース接続や、カーネル オブジェクトへのハンドルなどは考慮しません。次の 2 つのセクションで、メモリ管理とリソース管理をそれぞれ説明します。どちらも理解を深めるために不可欠なトピックです。
メモリ管理
ネイティブ C++ では、プログラマが、直接、メモリ管理を制御します。オブジェクトをスタックに割り当てるということは、関数呼び出しが行われると、そのオブジェクトのためのメモリが割り当てられ、関数が終了するとメモリが解放され、スタックも解放されるということを意味します。オブジェクトの動的な割り当ては、new 演算子を使用して行われます。このメモリは、CRT ヒープから割り当てられ、プログラマが、オブジェクトのポインタに対して delete 演算子を使用して明示的に解放します。このようなメモリに対する厳密な制御が、C++ が非常に効率的なコードを記述できる理由の 1 つでもありますが、プログラマの不注意でメモリ リークが発生してしまう原因でもあります。メモリ リークを避けるためにガベージ コレクタを採用する必要性はないかもしれませんが、これは、CLR が採用している手法であり、実際に非常に効率的です。もちろん、ヒープのガベージ コレクションには、他の利点もあります。たとえば、割り当てパフォーマンスの向上や、参照の局所性による利点などがあります。このような利点は、ライブラリ サポートによって C++ でも実現できますが、CLR の特徴は、すべてのプログラミング言語に共通の 1 つのメモリ管理プログラミング モデルを提供していることです。C++ での、COM オートメーション オブジェクト モデルからのデータ型の相互運用やマーシャルに必要な作業を考えると、この特徴の重要性が認識できます。複数のプログラミング言語に共通のガベージ コレクタの存在は、とても大きなものです。
CLR には、効率の問題が明らかであるため、値型を割り当てられる場所としてスタックの概念も残っています。しかし、CLR は、newobj 中間言語 (IL) 命令を備え、オブジェクトをマネージ ヒープに割り当てます。この命令は、C# の new 演算子を参照型に使用する際に提供されます。CLR には、C++ の delete 演算子と同等のものはありません。割り当てられたメモリは、アプリケーションが参照しなくなり、ガベージ コレクタが収集を行う決めたときに、収集されます。
マネージ C++ は、newobj 命令を、new 演算子が参照型に適用されたときにも生成します。しかし、delete 演算子を、マネージ ポインタ、またはガベージ コレクトされるポインタに使用するのは間違っています。これは、明らかに大きな矛盾です。また、C++ ポインタの概念で参照型を示すことが良くないと考えられる理由でもあります。
C++/CLI は、メモリ管理において、既にオブジェクトのコンストラクトの部分で説明した以外に新しく提供するものはありません。C++/CLI で本当にすばらしいのはリソース管理です。
リソース管理
リソース管理において、ネイティブ C++ に勝るものはありません。Bjarne Stroustrup の "Resource Acquisition Is Initialization" (リソース獲得は初期化時に行う) テクニックは、基本的には、それぞれのリソース型は、コンストラクタで 1 つのクラスとしてモデル化し、デストラクタでそのリソースを解放するということを示しています。これらの型は、スタック上のローカル オブジェクトとして使用される、あるいは、より複雑な型のメンバとして使用されます。その後は、デストラクタが、有効なリソースの自動的な解放を管理します。Stroustrup の表現を借りると、"C++ は、本来、ガベージ コレクションにとって、もっともすばらしい言語である。なぜなら、作るガベージが少ないから。" ということになります。
CLR が、リソース管理について、何の明らかなランタイム サポートも提供しないことについては、おそらく、多少、驚くことでしょう。CLR は、デストラクタという C++ の概念をサポートしません。.NET Framework は、IDisposable と呼ばれる中心となるインターフェース型に集中したリソース管理の形式を促進しています。これは、リソースをカプセル化した型で、このインターフェースの唯一のメソッドである Dispose を実装し、呼び出し元が、リソースを必要としなくなったときに、Dispose メソッドを呼ぶという仕組みです。C++ プログラマは、デフォルトでクリーンアップが正しいコードを記述するのに慣れているので、このようなステップは後退であると考える傾向にあるのも当然です。
リソースを解放するメソッドを呼び出す必要があるということについての問題は、例外に対応できるコードを記述するのが難しいということです。オブジェクトの Dispose メソッドの呼び出しを、単純に、コードの最後に置くことはできません。例外がどこかでスローされると、オブジェクトが保持するリソースがリークすることになります。C# は、これを解決するために、try-finally ブロック、および using ステートメントを提供し、例外が発生しても Dispose メソッドが確実に呼び出されるようにしています。これらの構成は、時には、大きな障害となることもありますが、記述する必要があります。記述しない場合、コードは、そのままコンパイルされ、デフォルトで静かなエラーを含んでいることになります。try-finally ブロック、および using ステートメントの必要性は、本当のデストラクタを備えていない言語での残念な必要性と言えます。
using (SqlConnection connection = new SqlConnection("Database=master; Integrated Security=sspi"))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "sp_databases";
command.CommandType = CommandType.StoredProcedure;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine(reader.GetString(0));
}
}
}
状況は、マネージ C++ でもほぼ同じです。try-finally ステートメントを使用する必要があります。このステートメントは、 C++ に対するマイクロソフトの拡張です。マネージ C++ は、C# の using ステートメントに相当するものは備えていません。しかし、単純な Using テンプレート クラスを作成し、それにより、GCHandle をラップし、マネージ オブジェクトの Dispose メソッドをそのテンプレート クラスのデストラクタから呼び出すようにするのは簡単です。
Using<SqlConnection> connection(new SqlConnection
(S"Database=master; Integrated Security=sspi"));
SqlCommand* command = connection->CreateCommand();
command->set_CommandText(S"sp_databases");
command->set_CommandType(CommandType::StoredProcedure);
connection->Open();
Using<SqlDataReader> reader(command->ExecuteReader());
while (reader->Read())
{
Console::WriteLine(reader->GetString(0));
}
C++ のリソース管理に対する従来からの強力なサポートを考えると、C++/CLI 言語設計が、C++ おけるリソース管理を簡単にするためにさまざまな対応をしているのも当然です。まず、リソースを管理するクラスの作成を見てみます。すべての場合とは言えなくても、ほとんどの場合において負担となるのは、CLR を対象とする言語で Dispose パターンを正しく実装することです。それは、ネイティブ C++ で従来のデストラクタを実装するように簡単にはできません。Dispose メソッドを作成する際は、基本クラス Dispose メソッドがあれば、それを確実に呼び出すようにする必要があります。さらに、Dispose メソッドを呼び出すことで、クラスの Finalize メソッドを実装する場合には、現在のアクセスを確認する必要があります。Finalize メソッドは、別個のスレッドで呼び出されるためです。また、Dispose メソッドが、実際に Finalize メソッドから呼び出される場合に、通常のアプリケーション コードと共に、マネージ リソースの解放に注意する必要があります。
C++/CLI は、これらの問題をすべて解決するわけではあリませんが、大きな支援となります。提供されるものを紹介する前に、今日の C# および マネージ C++ ではどのようにアプローチしているのか簡単に説明します。この例では、Base が IDisposable から派生しているものとします。もしそうでない場合は、Derived クラスが存在することになってしまいます。
class Derived : Base
{
public override void Dispose()
{
try
{
// マネージ リソース および アンマネージ リソースを解放
}
finally
{
base.Dispose();
}
}
~Derived() // Object.Finalize メソッドをインプリメント、まはたオーバーライド
{
// アンマネージ リソースのみを解放
}
}
マネージ C++ は、ほぼ同じです。デストラクタのように見えるのは、実は、Finalize メソッドです。コンパイラが、効率的に try-finally ブロックを挿入し、基本クラスの Finalize メソッドを呼び出すようにします。そのため、C# および マネージ C++ では、Finalize メソッドの作成は比較的簡単になりますが、おそらくより重要な Dispose メソッドの作成への支援はありません。Dispose メソッドは、擬似デストラクタとして、必ずしもリソースを解放せずに、単に、スコープの最後にいくつかのコードを実行するために使用されることがよくあります。
C++/CLI は、Dispose メソッドの重要性を認識し、それを、参照型の論理 "デストラクタ" としています。
ref class Derived : Base
{
~Derived() // IDisposable::Dispose メソッドをインプリメント、またはオーバーライド
{
// マネージ リソース および アンマネージ リソースを解放
}
!Derived() // Object::Finalize メソッドをインプリメント、またはオーバーライド
{
// アンマネージ リソースのみを解放
}
};
これは、C++ プログラマにとってより自然です。通常どおり、自分のリソースを自分のデストラクタで解放できます。コンパイラは、必要な IL を作り出し、IDisposable::Dispose メソッドを正しく実装します。これにより、ガベージ コレクタがオブジェクトの Finalize メソッドを呼び出すのを抑えられます。実は、C++/CLI で、明示的に Dispose メソッドを実装することは正当な方法ではありません。IDisposable からの継承は、コンパイラ エラーになります。当然、型がコンパイルされると、それを使用するすべての CLI 言語は、実装された Dispose パターンを各言語にもっとも自然な方法で参照します。C# では、Dispose メソッドを直接呼び出します。そうでない場合は、型が C# で定義されている場合と同じように、using ステートメントを使用します。C++ では、どうでしょうか。ヒープ ベースのオブジェクトのデストラクタを、通常、どのように呼び出すでしょうか。当然、delete 演算子を使用します。ハンドルへの delete 演算子の適用が、オブジェクトの Dispose メソッドを呼び出すことになります。オブジェクトのメモリは、ガベージ コレクタが管理していることに注意してください。そのようなメモリの解放は考慮しません。解放を考慮するのは、オブジェクトが保持するリソースのみです。
Derived^ d = gcnew Derived();
d->SomeMethod()
delete d;
つまり、delete 演算子に渡される式がハンドルである場合、オブジェクトの Dispose メソッドが呼び出されます。参照型への接続がもうなければ、ガベージ コレクタが適当な時点で、オブジェクトのメモリを自由に収集します。式がネイティブ C++ オブジェクトである場合は、メモリがヒープに戻される前に、オブジェクトのデストラクタが呼び出されます。
オブジェクトのライフタイムを管理する自然な C++ 構文により近くなりました。しかし、まだ、delete 演算子を呼び出す必要がある、というのはエラー発生の原因となります。C++/CLI では、参照型に対して、スタックの動作を取り入れることができます。つまり、オブジェクトをスタックに割り当てるために予約されている構文を使用する参照型を導入できるということです。コンパイラは、C++ に期待される動作を提供し、その一方で、実際にはオブジェクトをマネージ ヒープに割り当てることで、CLR の要件も満たします。
Derived d;
d.SomeMethod();
d がスコープ外になると、その Dispose メソッドが呼び出され、リソースを解放できるようになります。繰り返しますが、オブジェクトは、実際にはマネージ ヒープに割り当てられているので、ガベージ コレクタが独自のタイミングで、その解放を処理します。ADO.NET の例に戻ります。これは、C++/CLI では次のように記述されます。
SqlConnection connection("Database=master; Integrated Security=sspi");
SqlCommand^ command = connection.CreateCommand();
command->CommandText = "sp_databases";
command->CommandType = CommandType::StoredProcedure;
connection.Open();
SqlDataReader reader(command->ExecuteReader());
while (reader.Read())
{
Console::WriteLine(reader.GetString(0));
}
型についての再考
ボックス化について説明する前に、ここで、なぜ値型と参照型を区別する必要があるのかをもう一度確認しておきます。
値型のインスタンスは単純な値、参照型のインスタンスはオブジェクトと考えることができます。各オブジェクトは、オブジェクトのフィールドを保存するのに必要なメモリの他に、オブジェクト ヘッダーを保持します。これは、仮想関数によるクラスの継承、あらゆる場合に使用できるメタデータなど、オブジェクト指向プログラミングの基本的なサービスの提供を可能にします。しかし、間接的な仮想メソッドやインターフェイスに関連するオブジェクト ヘッダーは、静的な型の単純な値と、それに対するコンパイラ指定のいくつかの演算のみを必要とする場合には、メモリへのコストが大きすぎます。おそらく、コンパイラは、すべての場合ではなくても、状況よって、オブジェクトのコストを適切に削減できます。マネージ コードのパフォーマンスをまず考慮する場合には、値および値型の存在が有用なのは明らかです。ネイティブ C++ の型の体系には、このような大きな区別はありません。もちろん、C++ はどのようなプログラミングの規範も強制しません。そのため、C++ に加えるライブラリを作成することにより、独自の型の体系を構築することが可能です。
ボックス化
ボックス化とは、何でしょうか。ボックス化とは、値とオブジェクトの違いを埋めるメカニズムです。CLR では、すべての型は、Object から、直接的、または間接的に派生するようになっています。しかし、値は、実際にはそうではありません。スタック上の整数のような単純な値は、コンパイラが特定の演算を許可するメモリの 1 つのブロックに過ぎません。値を本当にオブジェクトとして扱う場合は、値が本当にオブジェクトである必要があります。値で、Object から継承されるメソッドを呼び出すことが可能でなければなりません。これを可能にするため、CLR はボックス化という概念を備えています。ボックス化が実際にどのように動作するかを知っておくと役に立ちます。まず、値は、ldloc IL 命令を使用してスタックに置かれます。次に、box IL 命令が使用されます。これは、大変な処理です。コンパイラは、値の静的な型、たとえば Int32 などを提供します。CLR は、先行して、スタックから値を取り出し、値とオブジェクト ヘッダーを保存するのに十分なメモリを割り当てます。新しく作成されたオブジェクトへの参照が、そのスタックに置かれます。ここまでの処理すべてが、box 命令によって行われます。最後に、オブジェクトの参照を取得するには、stloc IL 命令を使用して、スタックから参照を取り出し、ローカル変数に保存します。
ここで問題となるのは、値のボックス化を、プログラミング言語が暗黙的な演算とするか、明示的な演算とするかです。つまり、明示的な型変換をするか、または他の構造を使用するかということです。C# の言語設計では、暗黙の変換をするようになっています。最終的には、integer は、Object から非直接的に派生した Int32 型となります。
int i = 123;
object o = i;
問題は、すでにわかっているように、ボックス化は単純なコピーではないということです。これは、値からオブジェクトへの変換であり、コストが大きくなる可能性のある演算です。このため、マネージ C++ は、__box キーワードを使用する明示的なボックス化を採用しています。
int i = 123;
Object* o = __box(i);
マネージ C++ では、値をボックス化する際に、静的な型情報を失うことはありません。これは、C# にはない機能です。
int i = 123;
int __gc* o = __box(i);
厳密な記述によってボックス化された値は、dynamic_cast を使用することなく、値型に逆変換、つまり、ボックス化解除できるという利点があります。
当然ながら、マネージ C++ におる、この明示的なボックス化は、多くのケースで、構文的なオーバーヘッドが大きすぎることは明らかでした。このため、C++/CLI では、言語設計の方向を変更し、C# と同様の暗黙的なボックス化が取り入れられました。同時に、他の .NET 言語では表現できない厳密な記述のボックス化された値を直接表現することができるという点で、タイプ セーフであることは変わりません。
int i = 123;
int^ hi = i;
int c = *hi;
hi = nullptr;
ここから、オブジェクトを示していないハンドルは、ポインタと同様に、ゼロで初期化できないことがわかります。単に、ゼロという値をボックス化することになってしまうためです。これが、nullptr 定数の目的となるものです。これは、どのようなハンドルにも割り当てられます。C# では、null キーワードに相当します。nullptr は、C++/CLI 言語設計の中で新しく予約された単語ですが、Herb Sutter および Bjarne Stroustrup は、ポインタと一緒に使用するために、標準 C++ への追加を計画しています。
参照型および値型の作成
ここからは、CLR 型の作成の詳細について説明します。
C# では、参照型の宣言には class キーワードを使用し、値型の宣言には struct キーワードを使用します。
class ReferenceType {}
struct ValueType {}
C++ では、class および struct の両方に、すでに定義された意味があるので、どちらもこの目的で C++ で使用することはできません。当初の言語設計では、クラスの前に __gc キーワードを置いて参照型であることを示し、値型には、__value キーワードが使用されていました。
__gc class ReferenceType {};
__value class ValueType {};
C++/CLI は、ユーザー識別子と競合しない場所では、スペースで区切る複数のキーワードを導入しています。参照型を宣言するには、class または struct の前に ref を追加します。同様に、値型を宣言するには value を使用します。
ref class ReferenceType {};
ref struct ReferenceType {};
value class ValueType {};
value struct ValueType {};
class と struct の選択は、メンバのデフォルトでの可視性に関しては、これまでと同じです。主な違いは、CLR 型が public の継承のみをサポートすることです。private または protected の継承を使用するとコンパイラ エラーとなるので、明示的に public 継承を宣言することは、冗長ではなく、正当な記述です。
アクセシビリティ
CLR は、多くのアクセシビリティの変更を定義しており、これは、クラス メンバ関数および変数について、ネイティブ C++ の機能を超えるものです。また、入れ子になった型以外の、名前空間のアクセシビリティも定義できます。C++/CLI は、もっともロー レベルな言語であることという目標は達成しながらも、他の CLR を対象としたハイ レベル言語に比べて、より多くのアクセシビリティの制御を備えています。
ネイティブ C++ アクセシビリティと、CLR で定義されたアクセシビリティでは、大きく異なる点があります。ネイティブ C++ は、アクセス指定子を使用して、同じプログラムの他のコードからメンバにアクセスするのを制限します。一方、CLR は、同じアセンブリ内の別のコードだけでなく、参照してくる他のアセンブリに対しても、型およびメンバのアクセシビリティを定義する必要があります。
class や delegate 型など、名前空間、または入れ子になっていない型は、型定義の前に public または private を追加することにより、アセンブリの外側での可視性を指定できます。
public ref class ReferenceType {};
可視性を明示的に指定しない場合、型は、そのアセンブリに private であると想定されます。
メンバのアクセス指定子は拡張され、2 つのキーワードを同時に使用して、内部と外部のアクセスを、その後に続く名前に対して指定できるようになりました。2 つの定義のうちより厳しい方が、アセンブリ外部のアクセスを定義します。もう一方が、アセンブリ内部のアクセスを定義します。キーワードを 1 つだけ使用した場合は、内部および外部両方のアクセスにそのキーワードが適用されます。この設計により、作成した型やメンバのアクセシビリティの定義が大幅に柔軟になりました。次に例を示します。
public ref class ReferenceType
{
public:
// アセンブリの内部も外部も参照可能
private public:
// アセンブリ内部は参照可能
protected public:
// 外部の派生した型、および内部のすべてのコードに参照可能
};
プロパティ
入れ子になった型を別として、CLR の型は、メソッドとフィールドのみを保持します。プログラマの意図をより明確に伝えるため、プログラミング言語が特定のメソッドをプロパティとして処理することを示すためにはメタデータが使用できます。厳密には、CLR プロパティは、それを含む型のメンバですが、プロパティにはストレージが割り当てられません。つまり、プロパティを実装する各メソッドへの名前付きの単なる参照です。多くのコンパイラは、ソース コード内にプロパティに関する適切な構文が存在した場合には、要求されるメタデータを生成する必要があります。型のコンシューマは、その言語の適切な構文を使用して、プロパティを実装する get メソッド、および set メソッドにアクセスします。ネイティブ C++ とは異なり、C# のプロパティのサポートは優れています。
public string Name
{
get
{
return m_name;
}
set
{
m_name = value;
}
}
C# コンパイラは、対応する get_Name メソッド、および set_Name メソッドを生成し、その関連を示す必要なメタデータも含めます。マネージ C++ では、__property キーワードを使用して、そのメソッドがプロパティとしての動作を実装する役割があることを示します。
__property String* get_Name()
{
return m_value;
}
__property String* set_Name(String* value)
{
m_value = value;
}
当然、これは、理想的な方法ではありません。__property キーワードを使用しなければならない上に、この 2 つのメンバ関数が、実際には 1 つのグループであることが明確には示されません。これは、メンテナンスを重ねる中で、捕らえにくいバグをもたらす要因となります。C++/CLI のプロパティの設計は、より簡潔であり、C# の設計によく似ています。次に示すように、より強力でもあります。
property String^ Name
{
String^ get()
{
return m_value;
}
void set(String^ value)
{
m_value = value;
}
}
これは、すばらしい機能強化です。コンパイラは、get_Name メソッド、および set_Name メソッドを生成し、さらにこれがプロパティであることを宣言する必要なメタデータも生成します。自分のアセンブリ以外のコードにはプロパティの読み込みを許可し、アセンブリ内のコードには書き込みを許可することができれば、便利になる場合があります。これを実現するには、プロパティ名に続けて、中かっこの中にアクセス指定子を使用します。
property String^ Name
{
public:
String^ get();
private public:
void set(String^);
}
プロパティのサポートについて、最後に説明するのは、プロパティの取得および設定に、特別な処理を必要としない場合のための省略形の構文です。
コンパイラは、 get_Name メソッド、および set_Name メソッドをこれまでと同様に生成しますが、ここでは、プライベート String^ メンバ関数によってデフォルトの実装を提供します。これの利点は、この単純なプロパティならば、将来、より優れたプロパティ実装に置き換えることができ、クラスのインターフェースのコントラクトを破壊することもないということです。フィールドの簡単さと、プロパティの柔軟性が得られます。
デリゲート
ネイティブ C++ の関数ポインタは、コードを非同期的に実行するメカニズムを提供します。関数、またはより一般的にはファンクタへのポインタを保存し、タイミングを遅くして呼び出すことができます。これは、実装から、一部のアルゴリズム、たとえば、検索におけるオブジェクトの比較などを分離する方法として簡単に使用できます。また、別のスレッドでファンクタを呼び出し、本当に非同期なプログラムを作成するのにも使用できます。次に、ThreadPool クラスの簡単なサンプルを示します。ここでは、実行する関数へのポインタをワーカー スレッドで待たせます。
class ThreadPool
{
public:
template <typename T>
static void QueueUserWorkItem(void (T::*function)(),
T* object)
{
typedef std::pair<void (T::*)(), T*> CallbackType;
std::auto_ptr<CallbackType> p(new CallbackType(function, object));
if (::QueueUserWorkItem(ThreadProc<T>,
p.get(),
WT_EXECUTEDEFAULT))
{
// ここからは、ThreadProc がこのペアの削除を担当
p.release();
}
else
{
AtlThrowLastWin32();
}
}
private:
template <typename T>
static DWORD WINAPI ThreadProc(PVOID context)
{
typedef std::pair<void (T::*)(), T*> CallbackType;
std::auto_ptr<CallbackType> p(static_cast<CallbackType*>(context));
(p->second->*p->first)();
return 0;
}
ThreadPool();
};
C++ で、この ThreadPool を使用するのは、簡単で、自然です。
class Service
{
public:
void AsyncRun()
{
ThreadPool::QueueUserWorkItem(Run, this);
}
void Run()
{
// 何らかの長い処理
}
}
この ThreadPool クラスには、特定のシグネチャを持つ関数ポインタのみを処理する、という大きな制限があるのがわかります。これは、このサンプルの制限であり、C++ そのものの制限ではありません。ファンクタ全般については、Andrei Alexandrescu の 「Modern C++ Design」を参照してください。
C++ プログラマが、非同期プログラミング用の豊富なライブラリを取得し、実装するために、CLR に組み込みのサポートが付属しています。デリゲートは、関数ポインタに似ています。違うのは、メソッドをデリゲートに関連付けるのが可能かどうかは、対象となるオブジェクト、またはメソッドが所属する型に依存しない点です。メソッドは、シグネチャが一致する限り、遅延呼び出しのため、デリゲートに追加できます。これは、上記のサンプルと、C++ テンプレートを使用してどのクラスのメンバ関数でも使用されるようにしている点では (少なくとも雰囲気は) 同様です。もちろん、デリゲートは、このようなことを提供するだけではなく、非直接的なメソッド呼び出しには特に有効なメカニズムとなります。次に、C++/CLI を使用してデリゲート型を定義する例を示します。
delegate void Function();
デリゲートを使用するのは、簡単です。
ref struct ReferenceType
{
void InstanceMethod() {}
static void StaticMethod() {}
};
// デリゲートを作成し、メンバ関数のインスタンスに結びつける
Function^ f = gcnew Function(gcnew ReferenceType,
ReferenceType::InstanceMethod);
// さらに、デリゲートを組み合わせて、静的メンバ関数に結びつけ
// デリゲート チェーンを形成する
f += gcnew Function(ReferenceType::StaticMethod);
// invoke both functions
f();
まとめ
C++/CLI については、Visual C++ 2005 コンパイラを省いても、まだ説明することが多くあります。しかし、この記事は、CLR を使用するプログラマに C++/CLI がもたらすことについての説明の導入として役立ちます。この新しい言語設計は、これまでにないパワーとエレガンスさを備え、生産性、簡潔さ、またパフォーマンスのいずれも犠牲にすることなく完全な C++ での強力な .NET アプリケーションの作成を可能にします。
次の表は、もっとも一般的な構成をまとめたクイック レファレンスです。
| 説明 | C++/CLI | C# |
| 参照型の割り当て |
ReferenceType^ h = gcnew ReferenceType;
|
ReferenceType h = new ReferenceType();
|
| 値型の割り当て |
ValueType v(3, 4);
|
ValueType v = new ValueType(3, 4);
|
| 参照型 (スタックを使用) |
ReferenceType h;
| なし |
|
Dispose メソッドの呼び出し |
ReferenceType^ h = gcnew ReferenceType;
delete h;
|
ReferenceType h = new ReferenceType();
((IDisposable)h).Dispose();
|
|
Dispose メソッドのインプリメント |
~TypeName() {}
|
void IDisposable.Dispose() {}
|
|
Finalize メソッドのインプリメント |
!TypeName() {}
|
~TypeName() {}
|
| ボックス化 |
int^ h = 123;
|
object h = 123;
|
| ボックス化解除 |
int^ hi = 123;
int c = *hi;
|
object h = 123;
int i = (int) h;
|
| 参照型の定義 |
ref class ReferenceType {};
ref struct ReferenceType {};
|
class ReferenceType {}
|
| 値型の定義 |
value class ValueType {};
value struct ValueType {};
|
struct ValueType {}
|
| プロパティの使用 |
h.Prop = 123;
int v = h.Prop;
|
h.Prop = 123;
int v = h.Prop;
|
| プロパティの定義 |
property String^ Name
{
String^ get()
{
return m_value;
}
void set(String^ value)
{
m_value = value;
}
}
|
string Name
{
get
{
return m_name;
}
set
{
m_name = value;
}
}
|
執筆者紹介
Kenny Kerr は、Microsoft Windows プラットフォームでの分散アプリケーションの設計、および構築に携わっています。特に、C++ とセキュリティリ プログラミングを好んでいます。Reach Kenny については、http://weblogs.asp.net/kennykerr/
を参照してください。また、http://www.kennyandkarin.com/Kenny/
も参照してください。