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と変えてしまうと、参照ではなく値を渡すことになるため、sは配列にセットされた文字列ハンドルをコピーするだけとなる。そのため、arrの各要素は初期化されないままになってしまう。
int main()
{
array<String^>^ arr = gcnew array<String^>(10);
int i = 0;
for each (String^% s in arr)
s = gcnew String(i++.ToString());
}
加えて上記のコードは.NET言語の間でも表現力に差があるという例になる。つまり、C#ではforeach (ref string s in arr)
というようなforeachループが参照で値を読み書きする構文がなく、この場合はその他の回避策が使われる。
ファイナライザと自動変数
そのほかの変化として、C++/CLIではガベージコレクション時に実行されるファイナライザの構文が!クラス名()となったことが挙げられる。そして~クラス名()は従来のC++と同じ意味のデストラクタとなった。さらに、下の例にあるような新しい構文では、従来のC++と同じくデストラクタは自動的に呼ばれる。CIL上では、C++/CLIのデストラクタはIDisposableインターフェイスのDisposeメソッドとして実装される。C++/CLIコンパイラがそのようにコンパイルする。このためC++/CLIでも引き続きRAIIが可能である。
// C++/CLI
ref class MyClass // : IDisposable // これはコンパイラが追加する
{
public:
MyClass(); // コンストラクタ
~MyClass(); // デストラクタ(コンパイラによってIDisposable::Dispose()にされる)
static void Test()
{
MyClass x; // ハンドルでなく初期化子も無い:コンパイラがコンストラクタを呼ぶ。
// 変数xは、このメソッド内であればuserと同じように使える。
MyClass^ user = gcnew MyClass();
try { /* userを使う */ } finally { delete user; }
// コンパイラはメソッド全体を包むfinallyを作り、その中で自動変数xのデストラクタを呼ぶ。
}
protected:
!MyClass(); // ファイナライザ(マネージ拡張C++ではvirtual void Finalize()という構文だった)
};
// C#
class MyClass : IDisposable
{
public MyClass() {} // コンストラクタ
~MyClass() {} // デストラクタ(C++/CLIのファイナライザはprotected override void Finalize()に相当)
public void Dispose() {} // Dispose メソッド(C++/CLIではデストラクタ)
public static void Test()
{
using (MyClass auto = new MyClass()) { /* autoを使う */ }
// コンパイラはusingブロックを抜けるときにauto.Dispose()を呼ぶ。
// つまり下のコードに等しい。
MyClass user = new MyClass();
try { /* userを使う */ } finally { 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%)を引数に取るという手段も考えられる。そのような例は、演算子ではないがコピーコンストラクタや代入演算子の実装で使われることが考えられる。