コンテンツにスキップ

C++/CLI

出典: フリー百科事典『ウィキペディア(Wikipedia)』

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 2.0のジェネリクスに対応する構文も追加された。

C++/CLIも標準C++とは概ね互換性があるが、C++11で追加された<atomic>など、一部の標準ライブラリを使ったコードをコンパイルすることができないなどの問題があった。Visual Studio 2022 (17.6) ではC++/CLIモードがC++20モードとの併用に対応し、また標準C++との互換性問題が緩和されている[6]

新しい構文機能やキーワードは、C#の影響を受けているものも多い。なお、C++/CLIの独自拡張機能のうち、nullptroverrideenum class、委譲コンストラクタのように、のちに標準C++に取り込まれたものもいくつかある[7][8]。ただし、enum classはC++/CLIと標準C++とで互換性がなく、区別するためにC++/CLIコードの修正(マネージ列挙型に対するアクセシビリティ指定子の明示的な指定)が必要になるケースもある[9]

Microsoft Windows 8で導入されたWindowsランタイム (WinRT) を利用するコードをC++で効率的に記述できるようにするために、Visual Studio 2012では新たな独自拡張言語としてC++/CX英語版[10]のサポートが追加されたが、このC++/CXもC++/CLIとよく似た構文を採用している。ただしC++/CLIはマネージ言語拡張であるのに対し、C++/CXはネイティブ言語拡張である[注釈 3]

用途

[編集]

C++/CLIはC++マネージ拡張と同様に、マネージコードとネイティブコード(アンマネージコード)を混在して記述することのできる唯一の.NET言語である[12]。主にC言語C++のようなネイティブ言語で書かれたコード資産を、C#Visual Basic .NET (VB.NET) のようなマネージ言語から利用するために使われるが、逆にマネージ言語で書かれたコード資産をC++から利用するための混在コードを記述することも可能である。.NETには他にもP/InvokeCOM相互運用などの手段も用意されているが、C++/CLIでネイティブコードをラップしてマネージアセンブリ(DLL)を作成する方法は、より細やかな制御を可能とし、.NET言語から直接扱いやすいプログラミングインターフェイスを提供することが可能となる。

反面、.NETアプリケーションコードの効率的な記述能力やRAD対応はC#やVB.NETに劣り、統合開発環境 (IDE) の支援を受けられないことが多い。Visual Studio 2010まではWindows Forms関連のプロジェクトテンプレートがC++/CLI向けにも提供されていたが、Visual Studio 2012では削除された[13]WPF関連のプロジェクトテンプレートは最初からサポートされていない。C++/CLIは、マネージコードとアンマネージコードの相互運用を行う目的でのみ使用することが推奨されている。

ハンドル

[編集]

マネージ拡張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#のrefVisual Basic .NETByRefに相当する。C++/CLIはハンドルへの追跡参照を示すのに^%という構文を使用する。これは標準C++で「ポインタへの参照」を表す構文*&に似ている。

下記のコードは追跡参照の使用例である。仮に、下のコードでString^% sString^ 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%) を引数に取るという手段も考えられる。そのような例は、演算子ではないがコピーコンストラクタや代入演算子の実装で使われることが考えられる。

脚注

[編集]

注釈

[編集]
  1. ^ 非標準のキーワードは__gc__valueのように、処理系のために予約された識別子[5]を使用することによって、標準C++のキーワードと衝突しないように配慮されていた。
  2. ^ 例えばabstract, sealed, generic, delegateといった独自のキーワードが追加されている。
  3. ^ C++/CXは、WinRT参照型の自動メモリ管理に参照カウントを暗黙的に使用する。そのため、C++/CLIのマネージ参照型と違って、循環参照が発生しないように配慮する必要がある[11]

出典

[編集]

関連項目

[編集]

外部リンク

[編集]