C++/CLI
C++/CLIは、.NET Frameworkの共通言語基盤 (CLI) に対応し、共通言語ランタイム (CLR) 上で実行されるプログラムを記述するために、マイクロソフトがC++を拡張したプログラミング言語である。前身であるC++マネージ拡張に比べて単純でわかりやすい構文になり、可読性も向上している。Microsoft Visual Studio 2005からサポートが追加された。
C++/CLIはEcma Internationalで標準化されている[1]。C++/CLIに対応したコンパイラとしてMicrosoft Visual C++ 2005 (8.0) 以降がある。ほかにもClang上で実装する試みも存在する[2]。
.NET Frameworkだけでなく.NETでも利用可能であり、.NET Core 3.1およびVisual Studio 2019 (16.4) からサポートが追加された[3]が、サポートされるプラットフォームはMicrosoft Windowsのみである[4]。
構文の変化
C++マネージ拡張がC++のスーパーセット(上位互換)指向であった[注釈 1]のに対し、C++/CLIは独立した別の言語である[注釈 2]。これにより、特に曖昧な識別子の削除や、.NET固有の機能追加に関連する大きな構文上の変更が入っている。
もっとも大きな構文の違いとしてはnew演算子が挙げられる。C++/CLIでは.NETの参照型のインスタンスを作るための演算子をgcnewに分離した。また、.NETのジェネリクスに対応する構文も追加された。
C++/CLIも標準C++とは概ね互換性があるが、C++11で追加された<atomic>
など、一部の標準ライブラリを使ったコードをコンパイルすることができないなどの問題があった。Visual Studio 2022 (17.6) ではC++/CLIモードがC++20モードとの併用に対応し、また標準C++との互換性問題が緩和されている[6]。
新しい構文機能やキーワードは、C#の影響を受けているものも多い。なお、C++/CLIの独自拡張機能のうち、override
やenum class
のように、のちに標準C++に取り込まれたものもいくつかある[7]。ただし、enum class
はC++/CLIと標準C++とで互換性がなく、区別するためにC++/CLIコードの修正が必要になるケースもある[8]。
Microsoft Windows 8で導入されたWindowsランタイムを利用するコードをC++で効率的に記述できるようにするために、Visual Studio 2012では新たな独自拡張言語としてC++/CX[9]のサポートが追加されたが、このC++/CXもC++/CLIとよく似た構文を採用している。ただしC++/CLIはマネージ言語拡張であるのに対し、C++/CXはネイティブ言語拡張である。
ハンドル
マネージ拡張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%) を引数に取るという手段も考えられる。そのような例は、演算子ではないがコピーコンストラクタや代入演算子の実装で使われることが考えられる。
脚注
注釈
出典
- ^ ECMA-372 - Ecma International
- ^ LLVM Europe 2012: A Portable C++/CLI Compiler : Alp Toker
- ^ .NET Core 3.1 の新機能 - .NET | Microsoft Learn
- ^ .NET Core for WindowsでC++が使用可能に - InfoQ
- ^ 識別子 - cppreference.com
- ^ C++20 Support Comes To C++/CLI - C++ Team Blog
- ^ スコープを持つ列挙型 [N2347] - cpprefjp C++日本語リファレンス
- ^ 方法: C++/CLI で列挙型を定義および使用する | Microsoft Learn
- ^ C++/CX Language Reference | Microsoft Learn