PURE C++

C++/CLI の紹介

Stanley B. Lippman


翻訳元: Pure C++ Hello, C++/CLI (英語)


目次

  1. C++/CLI とは何か
  2. C++/CLI の習得
  3. CTS に対する C++/CLI のマッピング
  4. CLI の詳細レベル
  5. 問題は何か
  6. 追加機能
  7. C++/CLI のまとめ

C++/CLI は、コンポーネントをベースとした内蔵タイプのダイナミック プログラミング言語であり、C# または Java 同様、C++ から派生しています。ただし、この 2 つの言語とは異なり、C++/CLI の場合、ISO-C++ への統合を目指す努力が続けられてきました。それには、昨今のプログラミング パラダイムをサポートするために、C/C++ プログラミング言語の発展の歴史的モデルが使用されています。 C に対して C++ があるのと同様に、C++ に対して C++/CLI があると言うことができます。もっと一般的な言い方をすれば、C++/CLI へとつながる進化を、次のような歴史的経緯の中に見出すことができます。

  • BCPL (Basic Computer Programming Language: 基本コンピュータ プログラミング言語)
  • B (Ken Thompson。もともと UNIX で開発された言語)
  • C (Dennis Ritchie。型および制御構造が B に追加された言語)
  • クラス付きの C (1979 年まで)
  • C84 (1984 年まで)
  • Cfront リリース E (1984 年まで。大学向け)
  • Cfront リリース 1.0 (1985 年より。一般向け) 20 周年めを迎える
  • MI (Multiple/Virtual Inheritance: 複数/仮想継承) プログラミング言語 (1988 年まで)
  • 一般プログラミング (1991 年まで) (テンプレート)
  • ANSI C++/ ISO-C++ (1996 年まで)
  • ダイナミック コンポーネント プログラミング (2005 年まで) (C++/CLI)

1. C++/CLI とは何か

C++/CLI は、タプルを表します。 C++ は、もちろん、ベル研究所の Bjarne Stroustrup によって発明された C++ プログラミング言語のことを言います。この言語は、実行可能ファイルの速度とサイズに合わせて最適化された静的オブジェクト モデルをサポートします。ただし、ヒープ割り振り以外の、プログラムの実行時の変更はサポートされません。そこでは、基盤をなすマシンへの無制限のアクセスは可能ですが、実行中のプログラム内のアクティブな型へのアクセスはきわめて限定されており、そのプログラムの関連インフラストラクチャには事実上アクセスできません。 Microsoft での私の元同僚であり、C++/CLI の主任設計者であった Herb Sutter は、C++ を具象言語と呼んでいます。

CLI とは、Common Language Infrastructure (共通言語インフラストラクチャ) の頭文字であり、ダイナミック コンポーネント プログラミング モデルをサポートする複数層からなるアーキテクチャのことです。多くの面で、これは C++ オブジェクト モデルをまったく逆にしたものにほかなりません。ランタイム ソフトウェア層である仮想実行システムは、プログラムと、基盤のオペレーティング システムの間で稼働します。基盤のマシンへのアクセスは、かなり制約を受けます。実行中のプログラム内のアクティブな型と、それに関連したプログラム インフラストラクチャ (どちらも、ディスカバリおよび構造として) へのアクセスは、サポートされます。スラッシュ (/) は、C++ と CLI を結び付けるバインディングを表します。このバインディングにまつわる詳細によって、この記事の全体的なトピックが構成されています。

したがって、C++/CLI とは何かという質問に対して推定される最初の答としては、それは、CLI のダイナミック コンポーネント オブジェクト モデルに対する、静的な C++ オブジェクト モデルのバインディングということになります。簡単に言うと、それは、C# または Visual BasicR ではなく、C++ を使用してどのように .NET プログラミングを行うかということです。 C# および CLI そのものと同様に、C++/CLI は、欧州電子計算機工業会 (ECMA) のもとで標準化を進められており、究極的には ISO のもとでも標準化されることになります。

共通言語ランタイム (CLR) は、Microsoft バージョンの CLI であり、WindowsR オペレーティング システム独自のものです。同様に、Visual C++R 2005 は、C++/CLI を実装したものです。

2 番目に推定される答としては、C++/CLI は、.NET プログラミング モデルを C++ 内に統合したものと、私なら回答します。その統合で用いるのは、 その昔ベル研究所において、テンプレートを使用する汎用プログラミングを当時の既存の C++ 内で統合したのと同じ方法です。このどちらのケースでも、既存の C++ コードベースと既存の C++ の専門技術に関して培った知識は活かされます。それが、C++/CLI の設計において不可欠な、基本的な要件でした。

ページのトップへ


2. C++/CLI の習得

CLI 言語の設計では、どの言語でも変わることのない 3 つの局面があります。その 3 つは、基盤をなす CTS (Common Type System :共通型システム) に対する言語レベル構文のマッピング、プログラマ処理に対して公開する基盤の CLI インフラストラクチャの詳細さのレベルの選択、および、CLI で直接サポートされている以上のものを提供する追加機能の選択です。

最初のものは、どの CLI 言語でもほとんど同じです。2 番目と 3 番目のものは、各 CLI 言語がそれぞれ他の CLI 言語に対して一線を画すポイントです。解決する必要のある問題の種類に応じて、いずれか 1 つの言語を選択するか、あるいは複数の CLI 言語を組み合わせて選択することになります。C++/CLI の習得では、その一環として、その設計のこのような局面を 1 つずつ理解していきます。

ページのトップへ


3. CTS に対する C++/CLI のマッピング

C++/CLI のプログラミングでは、その基盤である CTS を習得することが大切です。これは、以下の 3 つの一般クラス型に分かれます。

  • 多形的参照型。これは、すべてのクラス継承で使用します。
  • 非多形的値型。これは、数値型などの、実行時の効率が重視される具象型を実装するときに使用されます。
  • 抽象インターフェイス型。これは、インターフェイスを実装する一連の参照型または値型に共通する一連の操作を定義するときに使用されます。

このような、一連の組み込み言語型への CTS のマッピングという設計の局面は、すべての CLI 言語を通して共通しています。ただし、当然ながら、各 CLI 言語ごとに構文は異なります。たとえば、C# で次のように書いたとします。

abstract class Shape { ... } // C#

上記は、個々の形状オブジェクトの派生元となる Shape の抽象基本クラスを定義します。他方、C++/CLI で、次のように書いたとします。

ref class Shape abstract { ... }; // C++/CLI

上記は、まったく同じ基盤の CLI 参照型を指示します。この 2 つの宣言は、基盤の IL でもまったく同じように表されます。同様に、C# で次のように書いたとします。

struct Point2D { ... } // C#

上記は、具象の Point2D クラスを定義するのに対して、C++/CLI では次のように書くことができます。

value class Point2D { ... }; // C++/CLI

C++/CLI でサポートされるクラス型のファミリは、ネイティブ機能に対する CTS の統合を表し、それによって、選択する構文が決まります。たとえば、次のように書いたとします。

class native {};
value class V {};
ref class R {};
interface class I {};

CTS はまた、ネイティブの列挙とは若干異なった動作を行う列挙クラスもサポートします。そのサポートは、次のように、そのどちらに対しても提供されます。™

enum native { fail, pass };
enum class CLIEnum : char { fail, pass};

同様に、CTS は、ネイティブ配列とはやはり異なった動作を行う独自の配列型をサポートします。この場合も、次のように、Microsoft はこの両者に対するサポートを提供しています。

int native[] = { 1,1,2,3,5,8 };
array<int>^ managed = { 1,1,2,3,5,8 };

CLI 言語を別の言語と比較して、基盤をなす CTS に対するマッピングにもっと近いか、またはほぼそのものであるということはありません。各 CLI 言語は、基盤となる CTS オブジェクト モデルに対するビューを表します。

ページのトップへ


4. CLI の詳細レベル

CLI 言語の設計時に配慮する必要のある 2 番目の局面として、基盤をなす CLI 実装モデルをどの程度詳細に、この言語に組み入れるかという点があります。この言語は、どのような種類の問題を解決する作業を担当するのでしょうか。この言語は、そのために必要なツールを備えているのでしょうか。さらに、この言語は、どのような種類のプログラマにとって魅力的なのでしょうか。

マネージ ヒープ上で出現する値型の問題を例として取り上げてみます。値型は、次のような、いくつかの状況下でマネージ ヒープ上に発生すると考えられます。

  • 値型のインスタンスが Object に代入されるときや、指定変更されない値型を介して仮想メソッドが呼び出されたときに、暗黙のボックス化を通して。
  • その値型が、参照クラス型のメンバとしてサービスを提供するとき。
  • その値型が、CLI 配列の要素型として保管されるとき。

この種の値型のアドレスにプログラマが手を加えてもよいかどうかは、CLI 言語の設計時に検討の対象として取り上げる必要があります。

ページのトップへ


5. 問題は何か

マネージ ヒープ上に置かれているオブジェクトはすべて、ガベージ コレクタのスイープ圧縮段階で再配置されることになります。そのようなオブジェクトを指すすべてのポインタを、ランタイムで追跡して更新する必要があります。プログラマ自身が手動で追跡することはできません。したがって、マネージ ヒープ上にある値型のアドレスをとる許可を受けた場合、既存のネイティブ ポインタに加えて、追跡フォームのポインタが必要になります。

検討の対象とすべきトレードオフは何でしょうか。一方では、単純さと安全が存在します。そのいずれかに対するサポートを言語内に直接導入するか、または追跡ポインタ ファミリを導入すると、言語の複雑さが増大します。このようなサポートを取り入れないと、手の空いたプログラマの集団が増えます。手の込んだ作業の必要性が減るからです。しかも、そのような一時的な値型にプログラマからアクセスできるようにすると、プログラマ エラーが起きる可能性が高くなります。プログラマは、意図的であってもなくても、メモリに対して危険な行為をすることがあるからです。追跡ポインタのサポートを取り入れなければ、作成する実行時環境の安全性は高くなると考えられます。

他方、効率と柔軟性も検討の対象とする必要があります。同じ Object に値型を代入すると、そのつどその値の新しいボックス化が発生します。ボックス化された値型にアクセスできるようにすると、メモリ内更新が可能になるので、かなりのパフォーマンスの向上が期待されます。追跡ポインタのフォームなしでは、ポインタ算術を使用して CLI 配列を巡回することはできません。それは、CLI 配列は標準テンプレート ライブラリ (STL) の反復子パターンにそぐわないので、汎用アルゴリズムにのっとって稼働することを意味します。ボックス化された値型にアクセスできれば、設計上の柔軟性がかなり向上します。

Microsoft での決定に従って、次のように、C++/CLI においてマネージ ヒープ上の値型を処理するための一連のアドレッシング モードが用意されました。

int ival = 1024;
int^ boxedi = ival;

array<int>^ ia = gcnew array<int>{1,1,2,3,5,8};
interior_ptr<int> begin = &ia[0];

value struct smallInt { int m_ival; ... } si;
pin_ptr<int> ppi = &si.m_ival;

一般的な C++/CLI 開発者は、洗練されたシステム プログラマであり、その任務は、企業の将来を確立するための足固めとして役立つインフラストラクチャおよび組織上の基幹アプリケーションを提供することにあります。プログラマは、スケーラビリティおよびパフォーマンス上の課題をこなす必要があり、そのため、基盤をなす CLI のシステム レベルの洞察力を備える必要があります。 CLI 言語の詳細さのレベルは、そのプログラマの本質を反映します。

複雑さそのものが、品質にとってマイナスになることはありません。人間は、単細胞のバクテリアよりはるかに複雑ですが、それは決して悪いことではありません。しかし、単純な概念の表現が複雑化された場合、通常それは悪いこととみなされます。C++/CLI では、複雑な用件をスマートなやり方で表現しようと、CLI チームの努力が重ねられました。

ページのトップへ


6. 追加機能

設計上の 3 番目の局面は、CLI で直接サポートされているものを超越した言語別の機能層です。これには、言語レベルのサポートと、CLI の基盤をなす実装モデルとの間のマッピングが必要になると思われます。これは、到底不可能な場合もあります。なぜなら、言語は CLI の動作にはなじまないからです。その 1 例として、基本クラスのコンストラクタおよびデストラクタにおける仮想関数の解決があります。この場合に ISO-C++ セマンティクスを反映するには、各基本クラスのコンストラクタおよびデストラクタ内で仮想テーブルをリセットする必要があります。これは不可能です。というのは、仮想テーブルの処理は、個々の言語によってではなく、ランタイムによって管理されるからです。

したがって、この設計局面は、何を行うのが望ましいかと、何ができるかの間の妥協ということになります。 C++/CLI に用意されている追加機能は、次の 3 つのエリアに大別されます。

  • 特に参照型の場合の RAII (Resource Acquisition is Initialization: リソースの取得は初期化である) のフォーム。これは、不足しているリソースを保持しているガベージ収集型の決定論的なファイナライズと呼ばれている作業を自動化する機能を提供します。
  • C++ のコピー コンストラクタおよびコピー代入演算子に関連したディープ コピー セマンティクスのフォーム。ただし、このセマンティクスを値型に拡張することはできませんでした。
  • CLI 汎用メカニズムに加えて、CTS 型用の C++ テンプレートの直接サポート。それ以外に、CLI 型用の STL の検証可能バージョンも用意されています。

簡単な例を見てみましょう。それは、決定論的なファイナライズに関するものです。オブジェクトに関連したメモリがガベージ コレクタによって回収されるときは、それに関連した Finalize メソッドがもしあれば、それが事前に呼び出されます。このメソッドは、オブジェクトの存続期間に結び付けられていないので、1 種のスーパー デストラクタと考えることができます。これをファイナライズと呼びます。 Finalize メソッドをいつ呼び出すかのタイミングや、あるいは呼び出すかどうかさえ、定義されていません。これが、ガベージ コレクタの非決定論的ファイナライズの真骨頂です。

非決定論的ファイナライズは、ダイナミック メモリ管理で威力を発揮します。使用可能メモリーがほとんどなくなると、ガベージ コレクタが割って入って、問題を解決します。しかし、データベース接続、何らかの種類のロック、またはネイティブのヒープ メモリなどの、基幹リソースがオブジェクトで保守されているときは、非決定論的ファイナライズは功を奏しません。そのような場合は、リソースがもう必要なくなったら、すぐにそのリソースを解放するのが得策です。 CLI で現在サポートされているソリューションとしては、クラスで、IDisposable インターフェイスの Dispose メソッドの実装内でリソースを解放します。この場合の問題は、Dispose は明示的な呼び出しを必要とするので、呼び出される見込みが薄いという点にあります。

C++ における基本的な設計パターンは、先に申し上げた RAII (Resource Acquisition is Initialization: リソースの取得は初期化である) です。これは、クラスはそのコンストラクタ内でリソースを取得することを意味します。これに対して、クラスは、そのデストラクタ内でリソースを解放します。これは、クラス オブジェクトの存続期間中は自動的に管理されます。

以下に、不足しているリソースの解放に関して、参照型が何を行うべきかを示してあります。

  • デストラクタを使用して、クラスに関連しているすべてのリソースを解放するのに必要なコードをカプセル化します。
  • クラス オブジェクトの存続時期間に結び付けられたデストラクタが自動的に呼び出されるようにします。

CLI には、参照型用のクラス デストラクタの考え方は備わっていません。したがって、基盤をなす実装内の何か他のものに対してデストラクタをマップする必要があります。というわけで、コンパイラが、内部で次のような変換を実行することになります。

  • IDisposable インターフェイスからの継承のために、クラスの基本クラス リストが拡張されます。
  • デストラクタは、IDisposable の Dispose メソッドに変換されます。

これで、目的地の半分まできました。さらに、デストラクタの呼び出しを自動化する手段が必要です。参照型用の特別なスタック ベースの表記法がサポートされています。つまり、それは、宣言の有効範囲内で存続期間が関連付けられているものです。内部的には、マネージ ヒープ上の参照オブジェクトを割り振るように、その表記はコンパイラによって変換されます。有効範囲の終了地点に、Dispose メソッド (ユーザー定義のデストラクタ) の呼び出しがコンパイラによって挿入されます。オブジェクトに関連付けられている実際のメモリの回収は、ガベージ コレクタの制御下に置かれたままになります。図 1 は、その例を示しています。

C++/CLI は、マネージ分野への C++ の単なる拡張ではありません。 C++/CLI は、完全統合のプログラミング パラダイムにほかなりません。そこには、継承と汎用プログラミングの複数のパラダイムをかつて言語に統合したことに似通った点があります。私は、チームが傑出した成果を上げたと思っています。

図 1 デストラクタの呼び出し

ref class Wrapper {
    Native *pn;
public:
    // RAII (Resource Acquisition is Initialization: リソースの取得は初期化である)
   Wrapper( int val ) { pn = new Native( val ); }

    // 以下は、ネイティブ メモリを処分します。
    ~Wrapper(){ delete pn; }

    void mfunc();
protected:

    // フェイルセーフとしての明示的な Finalize() メソッド
    !Wrapper() { delete pn; }
};

void f1()
{
   // 参照型の通常の処理
   Wrapper^ w1 = gcnew Wrapper( 1024 );

   // 存続期間に対する参照型のマッピング
   Wrapper w2( 2048 ); // no ^ token !

   // セマンティックの相違の説明
   w1->mfunc();
   w2.mfunc();

   // w2 はここで処分されます。
}

//
// ... 後で、w1 はいずれかの時点でファイナライズされると思われます。

ページのトップへ


7. C++/CLI のまとめ

C++/CLI は、ネイティブ プログラミングとマネージ プログラミングの統合を表します。繰り返して言うと、ある種の別々の、ただし同等の集まりである、ソース レベルと 2 進要素を通してその統合が成し遂げられています。そのような要素には、混合モード (ネイティブおよび CTS 型のソース レベルの混合、およびネイティブおよび CIL オブジェクト ファイルの 2 進混合)、Pure モード (ネイティブおよび CTS 型のソース レベルの混合。ただし、すべて CIL オブジェクト ファイルにコンパイルされます)、ネイティブ クラス (特殊ラッパー クラスのみを通して CTS 型を保持できます)、および CTS クラス (ポインタとしてネイティブ型のみを保持できます) などがあります。当然、C++/CLI プログラマは、選択によっては、CLI 型だけでプログラムすることもできます。それによって、たとえば SQL Server? 2005 内のストアド プロシージャとしてホストされることが可能な検証可能なコードが作成されます。

では、質問に戻りましょう。C++/CLI とは何でしょうか。それは、.NET プログラミング モデルへのファースト クラスの入国ビザです。 C++/CLI を使用すれば、C++ ソース ベースだけでなく、C++ の専門知識にも通じる C++ 移行道路が開けます。私にとって、それはとても喜ばしいことです。

ページのトップへ

ご質問およびご意見の送付先 purecpp@microsoft.com


Stanley B. Lippman は、1984年よりベル研究所にて、C++ の発明者である Bjarne Stroustrup とともに作業に従事していました。その後、同氏は、Disney および DreamWorks の両社で動画に携わり、「ファンタジア 2000」のソフトウェア テクニカル ディレクターを努めました。それ以降、同氏は、JPL の上級コンサルタントや、Microsoft の Visual C++ チームの設計者の役職に就いています。


この記事は、MSDN マガジン - Visual Studio 2005 ガイド ツアー号からの翻訳です。

QJ: 560002

ページのトップへ