Standard
ECMA-372
C++/CLI 言語仕様書
西暦2005年12月 初版
ナビゲーション リンクのスキップ

10.基本コンセプト

10.1 アセンブリ

 CLI では、ロード可能なコード・モジュールとリソースを機能の単体として一緒に実装した設定セットとして、アセンブリを定義しています。 C++/CLI プログラムはアセンブリ・マニフェストに含まれるファイル名によってアセンブリを認識しています。 アセンブリ・マニフェストはメタデータ中のアセンブリの名前のように、構成している全ての部分、アセンブリに寄与する他のファイル、そして、構成部分を検証する任意のハッシュ・コードを記述しています。
 アセンブリはアプリケーションかライブラリになることができます。 アプリケーションはアプリケーション・エントリ・ポイントを持ち、ライブラリは持っていません。

10.2 アプリケーション・エントリ・ポイント

 標準 C++ (§3.6.1 参照)に許された二つのメイン関数の定義に加えて、C++/CLI は以下の定義を許しています:
int main(array<System::String^>^ args) { /* ... */ }

 args の値はプログラムの引数を表す CLI 配列であり、インデックス 0 には第一引数を含んでいるべきでしょう。 もし、プログラムに引数が渡されなければ、args は 0 長配列となるべきです。 args は決して null にはなるべきでありません。 main に渡された配列は CLI ランタイムによって生成されます。 [注意:アプリケーション・エントリ・ポイントは CLI 標準の §15.4.1.2 に記述されています。]

10.3 アセンブリからの型取り込み

 おのおのの型定義はいくつかのアセンブリに属しています。そして、一つのアセンブリ中には一つ以上の型を含めることが可能です。 CLI 標準は多くの型を定義しており、おのおのの型は次の三つのアセンブリ:mscorlib.dll, System.dll, System.Xml.dll などの一つの中で定義されています。 アプリケーション・プログラマは必要に応じてその他のアセンブリを任意の個数作ることが可能です。
 #using 指定子はソース・ファイル中であるアセンブリ中の型を使用可能にします。 それはメタデータから型を import(取り込み)し、現在の翻訳単位の中にそのいずれの型も定義することはありません。 この指定子は次のような形を持ち、それらは等価です:
#using < assembly-name >
#using " assembly-name "
[注意:その見かけにかかわらず、#using は前処理(プリプロセッサ)の指定子ではありません。]

 アセンブリ mscorlib.dll 中の型はコンパイラによって暗黙のうちに取り込まれます。[例:
#using <mscorlib.dll>    // 余分なもの
#using <System.dll>      // Socket に必要
#using <System.Xml.dll>  // XmlTextReader に必要

int main() {
    System::Text::StringBuilder^ strBld;
    System::Net::Sockets::Socket^ soc;
    System::Xml::XmlTextReader^ xtr;
}
 それぞれの型が名前空間と親アセンブリ、そして、親ライブラリを持っています。 三つの文字列全ては分離しており、互いに関係していません。 例えば、Socket 型は名前空間 System::Net::Sockets 中にあり、アセンブリ Systemm.dll 中にあり、そのネットワーク・ライブラリ中に属しています。]
 メタデータの詳細は§34.1.1 を参照してください。

 #using 指定子があるアセンブリから型を取り込むとき、その型はそこに取り込まれた他のアセンブリの数によらず、そのアセンブリに所属し続けています。 他方、#include プリプロセス処理指定子が型定義を含むヘッダを持ち込む時には、その型定義はソース・コード中に取り込まれ、コンパイル時に現在の変換単位中の型として定義されます。
 アセンブリを #using する時、もし取り込まれる型が、この標準中で定義されていない modopt (§33.1 ) を含むシグネイチャを持った関数を持っていたり、この標準で定義されていない振る舞い(例えば、IsSignUnspecifiedByte (§33.1.5.7 ) が System::Byte や System::SByte 以外のものに使われていたり)が使われていたものだった場合、以下のルールが適用されます。
  • もし、その modopt を無視したとき、その型に他に同じシグネイチャがなければ、コンパイラはその modopt が存在しないかのようにシグネイチャを使うべきです。それから、もし、その関数が virtual であれば、どんな上書き関数にその modopt が繰り返されるべきです。
  • もし、その modopt を無視した関数のシグネイチャがその型中の違う関数のシグネイチャと同じであった場合、コンパイラは知らない modopt を持った関数を無視し、その関数は存在しないかのように扱います。
  • もし、未知の modopt を二つ以上のシグネイチャが持っていた場合、そして、modopt を持たないシグネイチャが存在しない場合、全ての関数は無視されます。
 アセンブリを #using する時、属性 NativeCppClass (§33.2.1 34.8 )を持つ任意の値クラスは、下記に書かれているとおり、ネイティブ・クラスとして扱われます。(もし、値クラス以外の型がその属性を宛われた場合、その属性は無視され、その属性を持っていないものとして扱われます。)
  • 違うアセンブリから #using によって持ち込まれた値クラスは、その型の事前宣言となります。
  • もし、クラスの宣言がソース・コード中にあれば、もし以下の基準に適合すれば、持ち込まれたものと同じクラスとして扱います。
    • ソース・コード定義が #using で取り込まれたエンコーディングと同じ名前です。
    • ソース・コード定義のサイズがエンコーディング中の定義のサイズと一意に識別します。
    • 双方の可視性は同等である必要はありません。

 "同じもの"として扱う条件は以下の通りです:
  • 他のアセンブリから型を使うときはいつでも、(現在のアセンブリの)ソースコード中で定義された型は代用されます。これは型変換ではありません。
  • call (呼び出し)などによって命令のために型情報が必要となったときはいつでも、その使われている型は呼び出された関数と一致するでしょう。しかし、与えられた型は現在のアセンブリ中でマッチした型のオブジェクトで代用できます。
  • 型情報が現在のアセンブリから紹介されている時(つまり、関数パラメータメタデータ)はいつでも、その使われている型は現在のアセンブリからの型に違いありません。
  • 唯一の例外はref型クラス中の仮想上書き(virtual overriding)です。仮想関数のシグネイチャはオリジナルと一致するべきです。故に、もしそのシグネイチャがネイティブ型を含んでいたら、任意の関数上書きはそのエンコーティング中で同じ型が使われているべきです。
 非仮想関数を使ったすべてのネイティブ型へのアクセスは現在のアセンブリからの関数であるべきです。メンバ関数はそれぞれのアセンブリごとに private であるべきでしょう。
 アセンブリを #using するとき、もし、そのアセンブリが見つからなかったり、見つかっても CLI 標準において正規でないフォーマットであったりした場合、コンパイラは #error 指定子に対応しているかのように振る舞うべきである。

10.4 予約名

 C++/CLIにはあるプログラマが決して記述できない関数が存在しますが、それは他の言語からの変換器によって生成されたメタデータからその関数が取り込まれる必要があるかもしれないためです。[例:これは名前が予約されており、プログラマによって記述することができない場合に起こりえます。例えば、Finalize, Dispose、その他の演算子関数名]
 #using は C++/CLI で記述されることができない名前で型を取り込むことができます。C++/CLI プログラマは予約名が C++/CLI が与える意味を持たない場合において、演算中にそのような名前を使うことができます。
[例:もし、関数名 Finalize がSystem::Object からの Finalize メソッドを上書きしなければ、C++/CLI プログラマは !T 構文(§19.13.2 )を使用することなく Finalize 関数を呼び出すことができます。

 二番目の例では次のような C# クラスを含んでいます。
public class C : IDisposable {
    void IDisposable.Dispose() { }
    public void Dispose() { }
}
 関数 C::Dispose はその C# クラスを #using するとき、 C++/CLI から呼び出されることができます。なぜなら、C::Dispose は IDisposable::Dispose 関数を実装も、IDispose::Dispose を実装する何らかの関数を上書きもしていないためです。
 三番目の例としては、インポートされたクラスは暗黙的、明示的に変換演算子を同様に持っています。この場合、コンパイラは開発者が op_Implicit や op_Explicit を書くことを許すことから退去するべきでしょう。

 __identifier(§9.1.1 )を参照してください。

10.5 メンバ


10.5.1 値クラスメンバ

 値クラスのメンバとは値クラスの中で宣言されたメンバであり、直接的に値クラスの基底クラス System::ValueType を継承し、間接的に System::Object を継承しています。
 基本型のメンバは実装によって(§12.1 )基本型に別名付けられている値クラスのメンバに対応しています。[例: signed char のメンバは System::SByte 値クラスのメンバです。]

10.5.2 デリゲート・メンバ

 デリゲートのメンバはクラス System::Delegate から継承されたメンバ、public インスタンス・コンストラクタ、そして、 BeginInvoke, EndInvoke、そして、Invoke の public メソッドです(§34.14 )。

10.6 メンバ・アクセス

10.6.1 アクセス規則の宣言

 標準C++ (§10)で、access-specifier がメンバのアクセスを制御するために使われています。 この文法はアセンブリの概念に適応して、以下のように、拡張されました。
access-specifier :
  private
  protected
  public
  internal
  protected public
  public protected
  private protected
  protected private
 標準C++ (§11/1)において、各々のaccess-specifier(アクセス指定子)ごとにメンバのアクセス制御が定義されています。 アセンブリの追加に伴って、定義のリストは以下のように拡張されます。
 
  • private; 宣言されたクラスのフレンドとメンバによってのみ、その名前を使うことができます。これは private アクセスと呼びます。
  • protected; 宣言されたクラスのフレンドとメンバ、そして、このクラスから派生したクラスのフレンドとメンバにのみ、その名前を使うことができます。(11.5 参照)派生クラスの親アセンブリは protected アクセスになんの影響もありません。これはファミリ・アクセスと呼びます。
  • public; これは、アクセス制限なくどこからでも使える名前です。これは public アクセスと呼びます。
  • internal; これは、その親アセンブリ中でその名前を使うことができます。これをアセンブリ・アクセスと呼びます。
  • public protected もしくは、protected public; その親アセンブリ内と含まれたクラスから派生した型でのみその名前を使うことができます。これを「ファミリ、ないし、アセンブリ」アクセスと呼びます。
  • private protected もしくは、protected private; その名前は、その親アセンブリ内の含まれたクラスから派生した型の中でのみ使用することができます。これを「ファミリ、および、アセンブリ」アクセスと言います。
[注記:アクセス指定子は二つのキーワードを含んでいるため、親アセンブリ中でより制限の少ない二つの指定子が与えられると、親アセンブリの外側では、より制限された方の指定子が与えられます。]
 名前のオーバーライド(上書き)はオーバーライドする名前と異なるアクセス可能性を指定することが許されています。 順序化はより大きなアクセス可能性との間で区別を明らかにしてくれます。 A と B の二つのアクセス可能性が与えられたとして、もし、A が そのアセンブリの外部に対して、A のアセンブリ内部と同じかより少ないアクセスを許可していたら、A は B よりアクセスが狭くなっています。もし、A が そのアセンブリの外部に対して、A のアセンブリ内部と同じかより大きなアクセスを許している場合、A は B よりアクセスが広いです。アクセス可能性の「狭さ」と「広さ」はアクセス可能性の部分規則を意味しています。例として、protected は private より広く、protected は public より狭く、protected private はpublic protected より狭く、internal と protected にはなんの規則性もありません。[注記:一般的に、アクセス可能性の「広さ」、「狭さ」は共通言語仕様互換(CLS-compliant)ではありません。]二つのアクセス可能性になんの序列も存在していなければ、一方は他方を上書きするべきではありません。
 「より広い」、ないし、「より狭い」アクセス可能性の要請が設置された時、直接的に関連づけされたアクセス指定子のみを考えます。クラス・メンバや型へのアクセス可能性は閉じたエンティティの最初にチェックしたアクセス可能性で決定されているので、広さ・狭さの規則は閉じられたエンティティを考慮しません。
[例:以下のコードは正規です。
public ref struct B {
  ref struct NB {
    virtual void F();
  };
};
private ref class D : B {
  ref class ND : B::NB {
  public:
    virtual void F() override;
  };
};
 ND 中の上書き仮想関数 F は NB 中の仮想関数 F よりも狭いアクセス可能性を持つことはできません。 NB::F は public アクセス可能性を持っているので、ND::F も publis アクセス可能性を持たなければなりません。 D と ND は双方とも private アクセス可能性を持っていますが、「より狭い」規則は影響しません。]
 メタデータについては§34.7.2 を参照のこと。

10.7 名前検索

 CLI 標準(パーティションI、§8.10.4 )は基底クラスの名前検索に二つの異なったアプローチをサポートしています:
  • もし、派生メンバがhyde-by-name(名前隠蔽)のマークをされていたら、その基底クラスにある同名の関数は派生クラスから視覚可能ではありません。このアプローチは hidebyname と呼ばれます。
  • もし、派生メンバがhyde-by-name-and-signature(名前、シグネイチャ隠蔽)のマークをされていたら、そのとき、基底クラス中の同名で同じシグネイチャの関数は、派生クラスから視覚可能ではありません。このアプローチは hidebysig と呼ばれます。
 これら二種類の隠蔽の間の相違の実装は、元となる言語のコンパイラとリフレクション・ライブラリによって完全に提供されます。それ自体は VES には直接的な影響を与えません。
[注意:標準 C++ では検索の間の、候補セット中の関数が静的、virtualか非virtualか、が多重定義解析に何の影響もありません。]

 標準 C++ は hidebyname 検索を要請しています。そのため、ネイティブ・クラスのメンバ関数は hidebyname によって検索します。[例:次のようなプログラムに関して、
struct B {
    void F(int i) { ... }
};

struct D : B {
    void F(String^ d) { ... }
};

int main() {
    D d;
    d.F(100);
}
 関数 F(String^) が見つかり、それは、互換性がないため、結果としてエラーになります。]
 他方、ref 型クラス、値クラス、インターフェイス・クラス、そして、デリゲート(それは本当に ref 型クラスとして実装されているため)のメンバ関数は hidebysig 検索を使います。[例:次のようなプログラムについて、
ref struct B {
    void F(int i) { ... }
};

ref struct D : B {
    void F(String^ d) { ... }
};

int main() {
    D d;
    d.F(100);
}
 関数 F(int) が呼び出されます。]
 
 もし、名前の検索があるクラス中で始まると、基底インターフェイスは無視されます。
 もし、名前の検索があるインターフェイス中で始まると、そのインターフェイスの基底に検索が進んだ時、それら基底クラスのインターフェイスにも検索が続けられるべきです。

 標準 C++ (§3.4/1)は明言しています。
アクセス規則(§11)は、名称検索と(もし適切であれば)多重定義解決が成功するのはただ一回だけであると考えられています。
 C++/CLI においては、そのルールはネイティブ・クラスに対してのみ適用されます。それとは異なり、CLI クラス型に対しては、アクセス不能な関数は名前検索からは見えません。 [注意:標準 C++ においては、private 名称は基底クラス中に隠すことができます。それに対して、CLI クラス型では private 名称は基底クラス中に隠すことができません。]
[注意:hidebyname において、名前検索はスコープ中においてその名前が見つかるやいなや中断します。hidebysig においては、マッチする名前がもうなくても名称検索は続行されます。]
 修飾名検索には、検索は指定されたスコープから始まります。もしそのスコープが hidebysig 規則を使うのであれば、その時、名前検索はその指定されたスコープと他のスコープ中の全ての名前を検索するために hidebysig 規則を使います。 [例:expr->R::F のような式は、もし、R が hidebysig クラスであれば、R から検索が始まります。通常の hidebysig ルールが適用され、それゆえ、R の基底クラス中に見つかる名前に含まれる一つの名前のセットすることが可能となります。]
 hidebysig ルールは基底クラス中の関数と、派生クラスの関数との間に曖昧さを生み出すことができるため、多重定義解決ルールは派生クラス中の関数を優先するよう拡張されています。[注意:多重定義解決は hidebyname 検索と hidebysig 検索とで生成されるオーバーロード・セットの候補について同等です。これが曖昧さを生みます。]
 C++/CLI は派生クラス中の関数を優先します。これを成し遂げるために、標準 C++ (§13.3.3)は次のように拡張されています。
与えられたこれらの定義に関して、実行可能な関数 F1 は全ての引数 i に対して ICSi(F1) は ICSi(F2) よりも適切な変換順序であるということから、もう一つの実行可能な関数 F2 よりも適切な関数であると定義されている。そして、それから、
――F1 は F2 より派生されたクラスのメンバであるか、F1 と F2 の双方が変換関数でないか、そうでなければ、
――いくつかの引数 j について、ICSj(F1) は ICSj(F2) よりも適切な変換順序にある、か、そうでなければ ...
[注意:そのルールによって、以下のプログラムは "float" を表示します。]
[例:
ref struct B {
    void F(double) { Console::WriteLine("double"); }
};

ref struct D : B {
    void F(float) { Console::WriteLine("float"); }
};

int main() {
    D d;
    d.F(3.14);
}
 (D^, double) から (B^, double) と (D^, float) への変換は同じランクにあります。故に、追加のルールがなければ、呼び出しは曖昧になってしまいます。]
 もし、あるクラス中の検索が関数でないエンティティを見つけたら、検索は基底クラスに対して続けません。もし、検索が派生クラス中で始まっており、検索セットはすでに関数を含んでいたのであれば、そのエンティティ名は名称セットに付け加えられることはありません。(検索の目的としては、プロパティとイベントはフィールドとして扱われます。)[例:
ref struct A {
    void F(Object^) { Console::WriteLine("A::F"); }
};

ref struct B : A {
    int F;
};

ref struct C : B {
    void F(String^) { Console::WriteLine("C::F"); }
};

int main() {
    C c;
    c.F(4);    // エラー
}
 C から始まった検索は関数 F を見つけることができず、関数が見つかったとき、同じ名前のフィールドが存在しているため、検索は B 中で停止します。B::F がプロパティでもイベントでも同様のことが発生します。]
 関数スコープはつねに hidebyname です。それゆえに、検索は関数スコープ中に名前を見つければ、その先に検索を続けたりしません。[例:
ref struct R {
    void F(Object^) { Console::WriteLine("R::F(Object^)"); }

    void F() {
        extern void F(String^);
        F(4);    // エラー
        Console::WriteLine("R::F()");
    }
};

int main() {
    R r;
    r.F();
}

void F(String^) { Console::WriteLine("::F(String^)"); }
 引数 4 は String^ に変換することができない上に、それが検索された唯一有効な関数であるために、プログラムは不正となります。]
 同じ名前空間中に二つ以上のジェネリック型の定義を含み、同じ名前空間中に違うアリティのジェネリック型の定義を持つプログラムは、不正です。 しかしながら、C++/CLI プログラムではそのような型を #using によって違うアセンブリから取り込むことが可能です。 これが発生した時、あいまいさは型引数の数を数えることで解決されるべきです。

文責:翻訳 => ヽ(゚∀。)ノうぇね