C++/CLI
C++/CLIは、.NET Frameworkの共通言語基盤 (CLI) 上で実行するプログラムを作るためにC++を拡張したプログラミング言語である。前身であるC++マネージ拡張に比べて単純でわかりやすい構文になり、可読性も向上している。
C++/CLIはEcma Internationalで標準化されている[1]。C++/CLIに対応したコンパイラとしてVisual C++ 2005以降がある。ほかにもClang上で実装する試みも存在する[2]。
構文の変化編集
C++マネージ拡張がC++に独自拡張を加えたスーパーセットであったのに対し、C++/CLIはそれ自身が1つの言語である。ただしC++とは上位互換である。それにより曖昧な識別子がなくなったり、.NET固有の仕様に適合するような機能の追加が行われるなどの大きな変更も加えられている。
もっとも大きな構文の違いとしてはnew演算子が挙げられる。C++/CLIでは.NETの参照型のインスタンスを作るための演算子をgcnewに分離した。また、.NETのジェネリックに対応する構文も追加された。
ハンドル編集
マネージ拡張C++には、2種類のポインタが存在した。従来からのC++ポインタである__nogc
ポインタと.NETの参照型オブジェクトを指す__gc
ポインタである。一方C++/CLIでは、ポインタはC++のポインタしかなく、.NETの参照型のオブジェクトを指すものは「ハンドル」と呼称することになった。ハンドル型はクラス名*に代わってクラス名^という構文を使う。これにより、.NETでガベージコレクションされるオブジェクトとそうでないものとが明確になり、マネージドとアンマネージドが混合しているコードが分かりやすくなった。gcnew は、C#でのnew
に相当する。またハンドルからメソッドやプロパティへのアクセスはアロー演算子 (->
) を用いる。
// マネージ拡張C++
#using <mscorlib.dll>
using namespace System;
using namespace System::Collections;
__gc class ReferenceType
{
private:
String* stringVar;
int intArr __gc[];
ArrayList* doubleList;
public:
ReferenceType(String* str, int* pointer, int number) // どれがマネージ型だろうか?
{
doubleList = new ArrayList();
intArr = new int __gc[8];
Console::WriteLine(String::Concat(str->Trim(), number.ToString()));
}
};
// C++/CLI
#using <mscorlib.dll>
using namespace System;
using namespace System::Collections::Generic;
ref class ReferenceType
{
private:
String^ stringVar;
array<int> intArr;
List<double>^ doubleList; // ジェネリック型の構文が追加された
public:
ReferenceType(String^ str, int* pointer, int number) // 区別が容易
{
doubleList = gcnew List<double>();
intArr = gcnew array<int>(8);
Console::WriteLine(str->Trim() + number); // Stringの連結に+演算子が使用可能となった
}
};
追跡参照編集
C++/CLIの追跡参照(トラッキング参照)は値ではなく参照で渡されるハンドルである。これらはC#のref
やVisual Basic .NETのByRef
に相当する。C++/CLIはハンドルへの追跡参照を示すのに^%
という構文を使用する。これは標準C++で「ポインタへの参照」を表す構文*&
に似ている。
下記のコードは追跡参照の使用例である。仮に、下のコードでString^% s
をString^ s
に変えてしまうと、参照ではなく値を渡すことになるため、s
は配列にセットされた文字列ハンドルをコピーするだけとなる。そのため、arr
の各要素は初期化されないままになってしまう。
{
array<String^>^ arr = gcnew array<String^>(10);
int i = 0;
for each (String^% s in arr)
s = i++.ToString();
}
加えて上記のコードは.NET言語の間でも表現力に差があるという例になる。C#のforeach文ではforeach (ref string s in arr)
というようにコレクション要素を参照として取得することができないため、例えば以下のような回避策を使うしかない。
{
string[] arr = new string[10];
for (int i = 0; i < arr.Length; ++i)
arr[i] = i.ToString();
}
C++/CLIには、C#のout
パラメータ修飾子に直接相当する構文は存在しない。C#を含む他の.NET言語と相互運用する際に必要な場合、属性構文[System::Runtime::InteropServices::Out]
を使い、メソッド引数を方向属性System::Runtime::InteropServices::OutAttribute
で修飾する。
ファイナライザと自動変数編集
そのほかの変化として、C++/CLIではガベージコレクション時に実行されるファイナライザの構文が!クラス名()となったことが挙げられる。そして~クラス名()は従来のC++と同じ意味のデストラクタとなった。さらに、下の例にあるような新しい構文では、従来のC++と同じくデストラクタは自動的に呼ばれる。共通中間言語 (CIL) 上では、C++/CLIのデストラクタはIDisposableインターフェイスのDisposeメソッドとして実装される。C++/CLIコンパイラがそのようにコンパイルする。このためC++/CLIでも引き続きRAIIが可能である。
// C++/CLI
// デストラクタを定義すると、IDisposableを明示的に指定しなくても、コンパイラが自動的にIDisposableを実装すると判断する。
ref class MyClass // : IDisposable
{
public:
MyClass() {} // コンストラクタ。
~MyClass() {} // デストラクタ。コンパイラによってIDisposable::Dispose()に変換される。
static void Test()
{
{
MyClass x; // ハンドルでなく初期化子も無い:コンパイラがコンストラクタを呼ぶ。
x.ToString();
// コンパイラはブロック全体を包むfinallyを作り、その中で自動変数xのデストラクタを呼ぶコードを自動生成する。
}
MyClass^ user;
try
{
user = gcnew MyClass();
user->ToString();
}
finally { delete user; }
}
protected:
!MyClass() {} // ファイナライザ。Object::Finalize()を直接オーバーライドすることはできない。マネージ拡張C++ではvirtual void Finalize()という構文だった。
};
// C#
class MyClass : IDisposable
{
public MyClass() {} // コンストラクタ。
~MyClass() {} // ファイナライザ(旧称デストラクタ)。Object.Finalize()を直接オーバーライドすることはできない。
public void Dispose() {} // IDisposable.Dispose() メソッドの実装。
public static void Test()
{
using (MyClass x = new MyClass())
{
x.ToString();
}
// コンパイラはusingブロックを抜けるときにx.Dispose()を必ず呼ぶコードを自動生成する。
// つまり以下のコードに等しい。
MyClass user;
try
{
user = new MyClass();
user.ToString();
}
finally { if (user != null) user.Dispose(); }
}
}
演算子の多重定義編集
アンマネージドのC++に関しては演算子の多重定義はおおむね正確に働く。すべての*
は^
となり、すべての&
は%
となるが、それ以外の構文はそのままでも多重定義を実装できる。また、それに加えてクラス自身に対してだけでなくそれらのクラスへのハンドルに対しても演算子多重定義が可能となった。従来のC++ではポインタ型同士に対して多重定義できなかった。また、CLIに適合するため演算子の多重定義をクラスの静的メンバとして実装することも可能になった。.NET Frameworkの参照クラスでもハンドルを引数に取る演算子の多重定義は静的メンバとして実装されている。
これは、中の文字列が同一なら、2つの異なるString
の参照を==
演算子で比較しても、String
の==
演算子の多重定義によって結果がtrueとなることを意味する。もちろん、マネージコードを書くときだけに限らず、常にそうあるべきであるように、演算子の多重定義は多態的でない。従って、Object^
へのキャストは多重定義のセマンティクスから逃れることになる。
//参照演算子の多重定義の効果
String ^s1 = "abc";
String ^s2 = "ab" + "c";
Object ^o1 = s1;
Object ^o2 = s2;
s1 == s2; // true
o1 == o2; // false
標準的なセマンティクスではネイティブ型や値型、仮に型Tに対しては、従来のC++のようにTやT const&を引数に取る演算子を定義し、参照クラス型Rに対してはハンドルR^を引数に取る演算子を定義することになる。ただ、C++だけのプロジェクトでは、ハンドル型を引数に取る演算子多重定義を使わないようにする、つまり参照クラスに対しても従来のC++の演算子の多重定義方式のように参照 (R const%) を引数に取るという手段も考えられる。そのような例は、演算子ではないがコピーコンストラクタや代入演算子の実装で使われることが考えられる。