31.ジェネリクス
ジェネリック型、そして、ジェネリック関数は--総称して
ジェネリクスと呼ばれている--型パラメーター化を許すために CLI に定義された特徴のセットです。
ジェネリクスは標準 C++ のテンプレートとは、テンプレートがコンパイラがコンパイル時にインスタンス化するのに対して、ジェネリクスは仮想実行システム( VES )が実行時にインスタンス化するという点で、異なっています。
ジェネリック宣言では一つ以上の
型パラメータを、ref型クラス、値クラス、インターフェイス・クラス、デリゲート、もしくは関数の宣言を、定義します。
ジェネリック宣言でジェネリック型や関数をインスタンス化するためには、ジェネリック宣言の型パラメータに対応した
型引数が与えられなければなりません。
型引数のセットは、どんな型パラメータでも構わないのですが、一つ以上の
拘束の利用して型に制限を課すことができます。
ジェネリック型のアリティはその型のために明示的に宣言された型パラメータの数です。
ネスト型のアリティは、それ自体、親の型によって導入された型パラメータに含まれません。
メタデータは
§34.18 を参照のこと。
31.1 ジェネリック宣言
ジェネリクスの追加に伴って、標準 C++ (§7 )の
declaration の文法定義は以下のように拡張されました。
declaration:
block-declaration
function-definition
template-declaration
generic-declaration
explicit-instantiation
explicit-specification
linkage-specification
namespace-definition
generic-declaration(ジェネリック宣言) は次のように定義されます。
generic-declaration:
generic < generic-parameter-list > constraint-clause-listopt declaration
generic-parameter-list:
generic-parameter
generic-parameter-list, generic-parameter
型パラメータは、一つ以上の
generic-parameter (
§31.1.1 )の順序列である
generic-parameter-list によって定義されています。拘束は
constraint-clause-list(
§31.4 )によって定義されています。
generic-declaration(ジェネリック宣言)の
declaration(宣言)がもし、ref クラス、値クラス、インターフェイス・クラス、デリゲート、もしくは、関数(コンストラクタ、デストラクタ、そして、ファイナライザは除外して)以外であった場合、そのプログラムは不正です。
もし、プロパティやイベントがジェネリックとして宣言された場合、プログラムは不正です。プロパティやイベントを構成する関数はジェネリックであるべきではありません。
generic-declaration は宣言です。
generic-declaration はまた、その宣言が ref型クラス、値クラス、インターフェイス・クラス、デリゲート、または、関数を定義するものであったら、それは定義でもあります。
generic-declaration は名前空間のスコープか、クラススコープの宣言にのみ現れるべきです。
ジェネリック非メンバ関数を除いて、、定義でもあるジェネリック宣言は public か private のアセンブリ可視化指定子(
§10.6.1 )を持つことができます。
ジェネリック型は同一スコープ中に置いて(標準 C++ 3.3 )、標準 C++ の 14.5.4 で規定されているものを除いて、その他のジェネリック型、テンプレート、クラス、デリゲート、関数、オブジェクト、列挙型、列挙子、名前空間、そして、型と同じ名前を持つべきではありません。ジェネリック関数は同名の非ジェネリック関数や、同名の他のジェネリック関数を上書きすることができますが、それを除けば、ジェネリック名称は名前空間のスコープやクラスのスコープ中でユニークであるべきです。
ジェネリック型宣言は注記した点を除けば、非ジェネリック型と同様の規則に従います。
ジェネリック型宣言は非ジェネリック型宣言中にもネストが可能です。ジェネリック型はネイティブクラス中にネストすることができます。
ジェネリック関数については後の章(
§31.3 )で議論します。
C++/CLI では、それぞれのジェネリック・パラメータ数が異なる同じスコープ中に同じ名前を持つ複数のジェネリック型を宣言することを許します。[例:
ref class R { ... };
generic<typename T>
public ref class R { ... };
generic<typename T, typename U>
public ref class R { ... };
]
ジェネリクスはアリティが違っていたとしても、与えられたスコープ中で視覚可能な違うスコープからのジェネリクスを作るために
using declaration(using 宣言)を使うべきではありません。同様にもし、違うスコープからのジェネリクスが
using-directive(using 指定子)によって検索され見つかったのであれば、名前検索は曖昧です。
ジェネリクスは厳密にも部分的にも特殊化することはできません。[注意:ジェネリクスは特殊化を許していないので、typename と template キーワードで名前の曖昧さを排除する必要はありません。]
ジェネリック関数やジェネリック・クラスはネイティブ・クラスの friend になることができます。ジェネリクスの全ての特殊化はフレンドを作成を作成するべきです。もし、ジェネリクスの何らかの特殊化がフレンドシップから排除されたら、そのプログラムは不正です。[注記:フレンドシップはネイティブ・クラスについてのみ認められます。そして、ネイティブ・クラスはジェネリクスになることはできません。ジェネリックによって他のクラスや関数とのフレンドシップを認めることはできません。]
31.1.1 型パラメータ
型パラメータは次のような方法で定義できます。
generic-parameter:
attributesopt class identifier
attributesopt typename identifier
class と typename は
generic-parameter(ジェネリック・パラメータ) にとって構文的な違いはありません。
generic-parameter(ジェネリック・パラメータ) はオプションとして一つ以上の属性(
§29 )を持つこともできます。
generic-parameter(ジェネリック・パラメータ) はその
identifier を
type-name であると定義しています。
generic-parameter(ジェネリック・パラメータ) のスコープは、それが宣言された場所から、
generic-parameter-list(ジェネリック・パラメータ・リスト) が与えられた
declaration(宣言)の終わりにまで広がっています。
[注意:テンプレートと違い、ジェネリクスは「型でない」
template-parameter(テンプレート・パラメータ) やテンプレート・
template-parameter(テンプレート・パラメータ) に等価なものを持っていません。また、ジェネリクスはデフォルト・ジェネリック・パラメータもサポートしていませんが、代わりに、ジェネリック型の多重定義が使えます。]
型として、型パラメータは純粋にコンパイル時生成です。実行時には、各型パラメータ毎に、ジェネリック型宣言に型引数として与えられることで実行時型に特殊化されて結びつけられます。それ故に、型パラメータで宣言された変数の型は、実行時に、閉じた生成型(
§31.2 )になるでしょう。型パラメータを含む全ての命令文と演算の実行時実行はそのパラメータに型引数で与えられた実際の型が利用されます。
もし、型パラメータがハンドル型として知られるものでない限り、リテラル nullptr はジェネリック型パラメータに与えて型として変換することはできません。しかしながら、デフォルト値演算をジェネリック型パラメータにヌル値を与える代わりに使うことができます。加えて、ジェネリック型パラメータによって型を与えられた値は、型パラメータが値型拘束(
§31.4 )を持っていなければ、 nullptr と == や != を使って比較することができます。[例:
generic<typename T, typename U>
where U : ref class
ref class R {
void F() {
T t = T(); // t はデフォルト値に初期化される
U u = nullptr; // u は nullptr で初期化することができる
// なぜなら ref class 拘束を持つので
/* ... */
}
};
]
ジェネリック型パラメータとして使われる任意の型は連携を持つべきです。
31.1.2 名前によるジェネリック型の参照
標準C++ のテンプレートのように、ジェネリック型 G<T> の本文中では、自身の型 G (generic-id や修飾されていない)の名前をどのように使おうと、現在のインスタンス化に関連しているものと仮定されます。[例、
generic<typename T>
ref class R {
public:
R() { } // ok: R<T>を意味している
void f(R^); // ok: R<T>を意味している
::R g(); // エラー
}
]
宣言の外側で、ジェネリック型は生成型(
§31.2 )を使って参照されます。[例:以下のように、
generic<typename T>
ref class List { };
generic<typename U>
void f() {
List<U>^ l1 = gcnew List<U>;
List<int>^ l2 = gcnew List<int>;
List<List<String^>^>^ l3 = gcnew List<List<String^>^>;
}
生成型のいくつかの例が List<U>、List<int>、そして、List<List<String^>^> です。一つ以上の型パラメータを使う生成型、List<U>のような、は開いた生成型(
§31.2.1 )です。List<int> のような、型パラメータを使わない生成型を閉じた生成型(
§31.2.1 )と呼びます。]
31.1.3 インスタンス型
型宣言毎に生成型に関連した、
instance type(インスタンス型)があります。
ジェネリック型宣言によって、インスタンス型は型宣言から、与えられた型パラメータに対応した型引数毎の生成型(
§31.2 )を作成して、形成されます。インスタンス型は型パラメータを使うので、インスタンス型は型パラメータがスコープ中にある場所、それはつまり、型宣言の中、でのみ使用することができます。
ref クラスの宣言中で、this はインスタンス型のハンドルです。
値クラスの宣言中では、this はインスタンス型の内部ポインタです。
非ジェネリック型では、インスタンス型は単純に宣言型となります。
[例:以下にインスタンス型を持ついくつかのクラス宣言を記します。
generic<type T>
ref class A { // インスタンス型は A<T>
class B { }; // インスタンス型は A<T>::B
generic<typename U>
ref class C { }; // インスタンス型は A<T>::C<U>
};
class D { }; // インスタンス型は D
]
31.1.4 基底クラスとインターフェイス
ジェネリック型宣言の基底クラスとインターフェイスは、型パラメータを使う生成型はなれますが、型パラメータになるべきではありません。[例:
ref class B1 {};
generic<typename T>
ref class B2 {};
generic<typename T>
interface class I1 {};
generic<typename T>
ref class R1 : T {}; // エラー(訳注:基底クラスに型パラメータを指定しているため)
generic<typename T>
ref class R2 : B1 {}; // ok
generic<typename T>
ref class R3 : B2<int>, I1<int> {}; // ok (閉じた生成型)
generic<typename T>
ref class R4 : B2<T>, I1<T> {}; // ok (開いた生成型)
]
ジェネリック・クラス宣言は、直接的、非直接的にかかわらず基底クラスに System::Attribute を使うべきではありません。
ジェネリック・クラス宣言は、テンプレート・パラメータである基底クラスを非直接的に持つべきではありません。
31.1.5 クラス・メンバ
ジェネリック型の全てのメンバは任意の閉じられた型から、直接的に、もしくは、生成型の一部としても、型パラメータを利用することができます。特定の閉じた生成型(
§31.1.2 )が実行時に利用されたとき、型パラメータのそれぞれの利用毎に生成型によって与えられた実際の型引数に置き換えられます。
プロパティ、イベント、コンストラクタ、デストラクタ、そして、ファイナライザはそれ自身の中に明示的に型パラメータを持つべきではありません。(それらはジェネリック・クラス内で起き、閉じられた型から型パラメータを利用することはできますが)
メンバの型が型パラメータである時、そのメンバの宣言は、任意のポインタや参照やハンドルの宣言なしで、型パラメータ名のみを使用するべきです。型パラメータの型を持つメンバへのアクセスは -> 演算子を使用します。[例:
interface class I1 {
void F();
};
generic<typename T>
where T : I1
ref class A {
T t; // *, &, %, ^ の宣言は許されていない
public:
void F() {}
void G() {
t->F(); // . ではなく -> が使われなければならない
}
};
]
[注意:コンパイラはメタデータ中にジェネリック型の定義を一つだけ生成します。ジェネリクスはジェネリック型パラメータとして値クラスを取ることを許しています。値クラスのパラメータの文字列置換では、メンバへのアクセスに -> 演算子を使うと不正なプログラムであると導かれるでしょう。VES はジェネリクスのインスタンス化に責任を負っているので、文字列置換はジェネリクスのインスタンス化について考えると悪いやり方と言えます。]
型パラメータを型に持つメンバは、値クラスであるか、ref クラス、インターフェイス・クラス、デリゲート、または、CLI 配列型のハンドルであるので、ジェネリック・クラスのデストラクタはそれらのメンバのデストラクタを呼び出しません。
ジェネリック・クラス定義の中で、継承された protected インスタンス・メンバへのアクセスは、ジェネリック・クラスから生成された任意の開いた生成クラス型のインスタンスを通して、許されます。[例:以下のコードに置いて、
generic<typename T>
ref class B {
protected:
T x;
};
generic<typename T>
ref class D : B<T> {
static void F() {
D<T>^ dt = gcnew D<T>;
dt->x = T(); // ok
D<int>^ di = gcnew D<int>;
di->x = 123; // エラー
D<String^>^ ds = gcnew D<String^>;
ds->x = "test"; // エラー
}
};
最初の x への代入はジェネリック型からの開いた生成クラス型のインスタンスを通して置き換えられているので許されています。しかしながら、二番目と三番目の代入は、閉じた生成クラス型のインスタンスを通して置き換えられるため、禁じられます。閉じた生成型ジェネリックのメンバにアクセスする時は、例えそれがジェネリック定義の中でも、アクセス規則はそのクラスが無関係なエンティティとして扱われるべきです。]
静的演算子については(
§31.1.7 )で議論します。その他の静的メンバについては(
§31.1.6 )で、ネスト型については(
§31.1.10 )で、そして、ジェネリック関数については、一般的に、(
§31.3 )で議論します。
31.1.6 静的メンバ
ジェネリック・クラス宣言中の静的データメンバは同じ閉じた生成型(
§31.1.2 )の全てのインスタンスで共有されていますが、異なる閉じた生成型のインスタンス間では共有されていません。これらのルールはその静的データ・メンバが型パラメータを含むか含まないかにかかわらず適用されます。
ジェネリック・クラスの静的コンストラクタは静的データメンバの初期化とそのジェネリック・クラス宣言で作成された異なる閉じた生成型それぞれの初期化を行うために使われます。ジェネリック型宣言の型パラメータは、静的コンストラクタの本体の中で、スコープ内にあり、使用することができます。
新しい閉じた生成型クラスは以下のどちらかで初回に初期化されます。
- 閉じた生成型のインスタンスが作成される時。
- 閉じた生成型の静的メンバのどれかが参照された時。
新しい閉じた生成型クラスを初期化するには、最初にその特定の閉じた生成型の静的データメンバの新しいセットが生成されます。それぞれの静的データメンバごとにそのデフォルト値に初期化されます。次に、静的データメンバ初期化子がそれら静的フィールドに実行されます。最後に、静的コンストラクタが実行されます。[例:
generic<typename T>
ref class C {
static int count = 0;
public:
static C() {
Console::WriteLine(<C<T>>::typeid);
}
C() {
count++;
}
static property int Count {
int get() { return count; }
}
};
int main() {
C<int>^ x1 = gcnew C<int>;
Console::WriteLine(C<double>::Count);
C<double>^ x2 = gcnew C<double>;
Console::WriteLine(C<double>::Count);
Console::WriteLine(C<int>::Count);
C<int>^ x3 = gcnew C<int>;
Console::WriteLine(C<double>::Count);
Console::WriteLine(C<int>::Count);
}
この出力は次のようになります。
C'1[System.Int32]
1
C'1[System.Double]
1
1
1
2
]
静的演算子は(
§31.1.7 )で議論します。
31.1.7 演算子
ジェネリック・クラス宣言では、非ジェネリック・クラス宣言と同じルールに従って、演算子を定義することができます。クラス宣言のインスタンス型(
§31.1.3 )は演算子の宣言中で
§19.7 の演算子や
§14.5.3 の変換関数のルールに応じて使われるべきでしょう。これらのルールに縛られていないパラメータはジェネリック型パラメータになり得ます。
[例:以下にジェネリック・クラスの正規な演算子定義の例をいくつか示します。
generic<typename T>
public ref struct R {
public:
static R^ operator ++(R^ operand) { ... }
static int operator *(R^ op1, T op2) { ... }
static explicit operator R^(T value) { ... }
};
]
31.1.8 メンバの多重定義(overload)
ジェネリック・クラス定義中の関数、インスタンス・コンストラクタ、静的演算子は多重定義することができます。ですが、これは閉じた生成型にいくつかのあいまいさを招くことになりえます。
[例:
generic<typename T1, typename T2>
ref class X {
public:
void F(T1, T2) { }
void F(T2, T1) { }
void F(int, String^) { }
};
int main() {
X<int, double>^ x1 = gcnew X<int, double>;
x1->F(10, 20.5); // OK
X<double, int>^ x2 = gcnew X<double, int>;
x2->F(20.5, 10); // OK
X<int, int>^ x3 = gcnew X<int, int>;
x3->F(10, 20); // エラー、あいまいである
X<int, String^>^ x4 = gcnew X<int, String^>;
x4->F(10, "abc"); // エラー、あいまいである
}
]
ジェネリック・クラスは潜在的にこれらの曖昧さを持つことを許しています。が、そのような曖昧な生成型を使用するプログラムは不正です。
31.1.9 メンバの上書き(override)
ジェネリック・クラスの関数メンバは普通に基底クラスの関数メンバを上書きできます。もし、基底クラスが非ジェネリック・クラスだったり、閉じた生成型である場合、任意の上書きする関数メンバは型パラメータを含む構成型をもてません。しかしながら、基底クラスが開いた生成型であれば、上書きする関数メンバは宣言中に型パラメータを使うことができます。上書きされる基底メンバを決定するとき、
§31.2.4 に書かれているように、基底クラスのメンバは型引数の置き換えによって確定されます。一度、基底クラスのメンバが確定すれば、上書きのルールは非ジェネリック・クラスと同様です。
[例:
generic<typename T>
ref class C abstract {
public:
virtual T F() { ... }
virtual C<T>^ G() { ... }
virtual void H(C<T>^ x) { ... }
};
ref class D : C<String^> {
public:
String^ F() override { ... } // OK
C<String^>^ G() override { ... } // OK
void H(C<int>^ x) override { ... } // エラー、C<String^> であるべき
};
generic<typename T, typename U>
ref class E : C<U> {
public:
U F() override { ... } // OK
C<U>^ G() override { ... } // OK
void H(C<T>^ x) override { ... } // エラー、C<U> であるべき
};
]
31.1.10 ネスト型
ジェネリック・クラス宣言は、ジェネリック・クラス宣言がネイティブ・クラスを含まない限りに置いて、ネスト型宣言を含むことができます。閉じた型の型パラメータはネスト型の中でも利用できます。ネスト型宣言ではそのネスト型だけに通じる追加の型パラメータを含むことができます。ジェネリック型は非ジェネリック型の中にネストすることができます。
ジェネリック・クラス宣言を中に含むすべての型宣言は暗黙のうちにジェネリック型宣言になります。ジェネリック型中にネスト型への参照を書く場合、含まれている生成型、その型引数も含む、は名前付けされていなければなりません。しかしながら、外側のクラスからはネスト型は完全修飾なしに使用することができます。外側クラスのインスタンス型は、ネスト型を生成するとき、暗黙のうちに使用されます。[例:以下の例では三つの違うInnerから作成された生成型への正確な参照方法を示しています。最初の二つは等価です。
generic<typename T>
ref class Outer {
generic<typename U>
ref class Inner {
public:
static void F(T t, U u) { }
};
static void F(T t) {
Outer<T>::Inner<String^>::F(t, "abc"); // これら二つの命令文は
Inner<String^>::F(t, "abc"); // 同じ効果を持つ
Outer<int>::Inner<String^>::F(3, "abc"); // これは型が違う
}
};
]
ネスト型中の型パラメータはメンバや外側クラスで宣言された型パラメータを隠します。[例:
generic<typename T>
ref class Outer {
generic<typename T> // 適正。Outer の T を隠す
ref class Inner {
T t; // Inner の T を参照する
};
};
]
クラス・テンプレート中にネスト化されたジェネリック型を持つプログラムは不正です。
31.2 生成型
ジェネリック型宣言は、型引数(
§31.2.1 )を適用することによって、多くの異なる型を形成する青写真として利用されます。少なくとも一つの型引数によって名前付けされた型を
生成型(constructed type)と呼びます。(
§31.2.1 )で見るように、生成型は開いた生成型か閉じた生成型になりえます。
ジェネリクスの追加に併せて、標準 C++ (§5.1 )の
unqualified-id(非修飾 id ) の文法は以下のように
generic-idを追加して拡張されます。
unqualified-id:
identifier
operator-function-id
conversion-function-id
~ class-name
! class-name
template-id
generic-id
default
生成型は
generic-id を参照します。
generic-id:
generic-name < generic-argument-list >
generic-name:
identifier
generic-argument-listについては(
§31.2.2 )で議論します。
31.2.1 開いた生成型、閉じた生成型
全ての型は
open constructed type(開いた生成型)か
closed constructed type(閉じた生成型)に分類されます。開いた生成型は型引数を含む型を持ちます。特記すると、
- 型パラメータは開いた生成型を定義する。
- CLI 配列はその要素型が開いた生成型であるときのみ、開いた生成型になる。
- 生成型は一つ以上の型引数を持ち、それが開いた生成型である時、その時に限り、開いた生成型になる。生成ネスト型は一つ以上の型引数(§31.2.2 )を持った時、または、その型に含まれる型引数が開いた生成型であるときに、開いた生成型になる。
閉じた生成型は開いた生成型でないものです。
[例:次に与える
generic<typename T>
ref class List {};
generic<typename U>
void f() {
List<U>^ l1 = gcnew List<U>;
List<int>^ l2 = gcnew List<int>;
List<List<String^> >^ l3 = gcnew List<List<String^> >;
}
List<U>, List<int>, List<List<String^>> が生成型の例となります。そのうち、List<U> は開いた生成型で、List<int>とList<List<String^>>が閉じた生成型です。]
実行時、ジェネリック型宣言中の全てのコードはジェネリック宣言に型引数が与えられることによって作られた閉じた生成型のコンテキスト中で実行されます。ジェネリック型の各々の型引数毎に特別な実行時型に結びつけられます。全構文と演算の実行プロセスでは常に閉じた生成型が発生しており、開いた生成型はコンパイル時過程にのみ現れます。
各々の閉じた生成型毎に、それ自身が静的変数のセットを持ち、他の閉じた生成型とは共有されません。開いた生成型は実行時には現れないため、開いた生成型と関連する静的変数はありません。同じ型宣言から生成され、対応した同じ型の型引数を持つ二つの閉じた生成型は同一の型です。
生成型はその最小型引数と同じアクセス可能性を保持します。
31.2.2 型引数
ジェネリック型やジェネリック関数はジェネリック宣言の型パラメータに対応した型引数を指定することによってジェネリック宣言からインスタンス化されます。型引数は
generic-argument-list を通して指定されます。
generic-argument-list:
generic-argument
generic-argument-list , generic-argument
generic-argument:
type-id
ジェネリック・クラスのインスタンス化引数は常に明示的に指定されるべきです。ジェネリック関数(
§31.3 )のインスタンス化引数は明示的に指定することもできますが、型推論によって決定することも可能です。
generic-argument(ジェネリック引数)は、値クラス、ref クラスのハンドル、デリゲートへのハンドル、インターフェイスへのハンドル、CLI 配列へのハンドル、ないし、閉じたジェネリックからの型引数であるに違いないとして、生成型になります。
[注意:ネイティブ・クラスやポインタ、参照、値クラスのハンドル、ボックス化値型、ないし、値渡しされた ref クラスについては、ジェネリック引数として使用できません。]
generic-argument(ジェネリック引数)毎に、対応する型パラメータに課せられた任意の拘束(
§31.4 )を満たすべきでしょう。
31.2.3 基底クラスとインターフェイス
生成型は直接的な基底クラスを持ちます。もし、ジェネリック・クラス宣言が基底クラスを指定していない場合には、基底クラスは System::Object になります。
もし、基底クラスがジェネリック・クラス宣言の中で指定されていた場合、生成型の基底クラスは、基底クラス宣言中の各々の
ジェネリック・パラメータ(generic-parameter)毎に対応した生成型のジェネリック引数を、置き換えることで獲得します。
[例:与えられたジェネリック・クラス宣言で
generic<typename T, typename U>
ref class B { ... };
generic<typename T>
ref class D : B<String^, array<T> > { ... };
生成型 D<int> は B<String^, array<int> >が基底クラスとなる。]
同様に、生成された ref クラス、値クラス、そして、インターフェイス型は明示的な基底インターフェイスのセットを持ちます。明示的な基底インターフェイスは、ジェネリック型宣言において明示的な基底インターフェイスを取り、基底インターフェイス宣言中の
generic-parameter(ジェネリック・パラメータ)毎に対応した生成型の
generic-argument(ジェネリック引数)に置き換えることによって形成されます。
型のための全ての基底クラスと基底インターフェイスのセットは、通常、直接の基底クラスとインターフェイスの基底クラスとインターフェイスを再帰的に取得することで形成されます。[例:例えば、与えられたジェネリック・クラス宣言に置いて:
ref class A { ... };
generic<typename T>
ref class B : A { ... };
generic<typename T>
ref class C : B<ICompatible<T>^> { ... };
generic<typename T>
ref class D : C<array<T> > { ... };
D<int> の基底クラスは、C<array<int> >、B<IComparable<array<int>^> >、A、そして、System::Object と再帰的に取得されていく。]
31.2.4 クラス・メンバ
生成型の非継承メンバは、メンバ宣言の
generic-parameter(ジェネリック・パラメータ)毎に対応したその生成型の
generic-argument(ジェネリック引数)に置き換えることによって獲得されます。
置き換えのプロセスは型宣言の構文上の意味を基盤としており、単なる文字列置き換え(
§31.1.5 )ではありません。
[例:与えられたジェネリック・クラス宣言に対して
generic<typename T, typename U>
ref class X {
array<T>^ a;
void G(int i, T t, X<U,T> gt);
property U P { U get(); void set(U value); }
int H(double d);
};
生成型 X<int, bool> は次のメンバを持っています。
array<int>^ a;
void G(int i, int t, X<int,bool>^ gt);
property bool P { bool get(); void set(bool value); }
int H(double d);
]
生成型の継承されたメンバは似たような方法で取得されます。
最初に、直接の基底クラスの全てのメンバが決定されます。
もしも、基底クラスがそれ自体生成型だったら、これには現在のルールの再帰的適用を含んでいるに違いありません。
そして、メンバ宣言中の
generic-parameter(ジェネリック・パラメータ)に対応した生成型の
generic-argument(型引数)への置き換えによって、各々の継承メンバが変形されます。[例:
generic<typename U>
ref class B {
public:
U F(long index);
};
generic<typename T>
ref class D : B<array<T>^> {
public:
T G(String^ s);
};
上の例で、生成型 D<int> は、型パラメータ T を型引数 int に置き換えることによって得られた int G(String^ s) の非継承メンバを持ちます。D<int> はまた、クラス宣言 B からの継承メンバを持っています。この継承メンバは、最初に生成型 B<array<T>^> のメンバを決定するために、 U を array<T>^ に置き換え、生じた array<T>^ F(long index) が確定されます。そして、型引数 int を型パラメータ T に置き換えることで、継承メンバ array<int>^ F(long index) がもたらされます。]
31.2.5 アクセス性
生成型 C<T1, ..., TN> はその部分である C, T1, ..., TN の全てがアクセス可能であるとき、アクセス可能となります。
例えば、もしジェネリック型名 C が public で、その全ての
ジェネリック引数(generic-argument)T1, ..., TN が public のアクセス可能性を持っているなら、その時、生成型のアクセス可能性は public となります。
が、もし、型名 C かジェネリック引数のどれかが private のアクセス可能性を持っていると、生成型のアクセス可能性は private になります。
もし、ジェネリック引数の一つが protected のアクセス可能性を持っており、もう一つが private protected のアクセス可能性を持っていたら、その時は、生成型はこのクラス内とこのアセンブリ中のサブクラスでのみアクセス可能となります。
生成型のアクセス可能領域は、開いた生成型とその型引数のアクセス領域のもっとも制限されたアクセスになります。ジェネリクスのインスタンス化のアクセス可能性ルールはテンプレートのものと同じです。
31.3 ジェネリック関数
メンバ関数と非メンバ関数はジェネリック宣言(
§31.1 )することができます。
ジェネリック関数が ref クラス、値クラス、または、インターフェイス宣言の中で宣言されたとき、閉じた型はそれ自体がジェネリックか非ジェネリックの双方になりえます。もし、ジェネリック関数がジェネリック型宣言中で宣言されたのなら、関数の本体は関数の型パラメータと型宣言に含まれた型パラメータの双方を参照することができます。必ずしもジェネリック関数の全てのジェネリック型パラメータが、パラメータ型として、またはその関数の返却型として現れる必要はありません。[例:
generic<typename T>
void f1(T);
ref class C1 {
generic<typename T, typename U>
T f2(T t) {
U u;
...
}
generic<typename T>
T f2(T);
};
generic<typename T1>
ref class C2 {
generic<typename T2>
void f3(T1, array<T2>^);
};
]
ジェネリック関数のパラメータ型として使われなかった型は推論できません。関数テンプレートのために推測できない型はジェネリック関数のためにも推測できません。
ジェネリック関数と使われた時、static, extern inline は同じコンテキストで非ジェネリック関数として使われた時と同じ意味を持ちます。
パラメータや変数の型が型パラメータである時、そのパラメータや変数の宣言はポインタ、参照、ないし、ハンドル宣言のどれでもなく、型パラメータ名を使うべきです。
型が型パラメータであるパラメータ、変数へのメンバ・アクセスは->演算子を利用するべきです。[例:
interface class I1 {
void F();
};
generic<typename T>
where T : I1
void H(T t1) { // *, & や ^ が宣言されてはいけない
T t2 = t1; // 同上
t1->F(); // . ではなく -> を必ず利用する。
t2->F(); // 同上
}
]
型パラメータはパラメータ配列の型にも利用できます。
ジェネリック関数は適切な型付けをしたデリゲートに結びつけることができます。
31.3.1 関数識別記述マッチ規則
関数の多重定義のシグネイチャ比較の目的で、任意の
constraint-clause-lists(拘束項リスト)は、関数の
generic-parameter(ジェネリック・パラメータ)の名前と同様、無視されます。
しかしながら、多くのジェネリック型パラメータは適切です。[例:
ref class A { };
ref class B { };
interface class IX {
generic<typename T>
where T : A
void F1(T t);
generic<typename T>
where T : B
void F1(T t); // エラー、拘束は無視される
generic<typename T>
T F2(T t, int i);
generic<typename U>
void F2(U u, int i); // エラー、パラメータ名と返却型は無視される
void F3(int x); // 型パラメータは存在しない
generic<typename T>
void F3(int x); // OK 違う型パラメータ数を持つ
generic<typename T, typename U>
void F3(int x); // OK 違う型パラメータ数を持つ
generic<typename U, typename T>
void F3(int x); // エラー、型パラメータ名は無視される
};
]
関数は多重定義が可能です。しかし、これは正確な呼び出しに曖昧さを招き得ます。[例:
generic<typename T1, typename T2>
void F(T1, T2) { }
generic<typename T1, typename T2>
void F(T2, T1) { }
int main() {
F<int, double>(10, 20.5); // okay
F<double, int>(20.5, 10); // okay
F<int, int>(10, 20); // エラー、あいまいである
}
]
プログラムはジェネリック関数宣言がそのような曖昧さを招くことを認めていますが、もし、関数呼び出しがそのような曖昧さを生んだ場合には不正となります。
ジェネリック関数は abstract, virtual, そして、override を宣言することができます。上述のシグネイチャ・マッチ規則は上書き時、または、インターフェイス実装時に利用されます。ジェネリック関数が基底クラスや基底インターフェイスの関数実装で宣言されたジェネリック関数を上書きするときには、各関数型パラメータに与えられた拘束は両方の宣言内で同一であるべきです。
[例:
ref struct B abstract {
generic<typename T, typename U>
virtual T F(T t, U u) abstract;
generic<typename T>
where T : IComparable
virtual T G(T t) abstract;
};
ref struct D : B {
generic<typename X, typename Y>
virtual X F(X x, Y y) override; // Okay
generic<typename T>
virtual T G(T t) override; // エラー、拘束がマッチしていない
};
F の上書きは型パラメータ名が違いを許しているので正当です。G の上書きは与えられた型パラメータの拘束が(この場合だと指定されていないために)上書きする関数のものと一致しません。
31.3.2 型推論(Type deduction)
ジェネリック関数の呼び出しは
generic-idを通して型引数リストが明示的に指定できます。または、型引数リストは
generic-nameのみを使うことで、型引数を決定するために
type deduction(型推論)によって省略することができます。
[例:
ref struct X {
generic<typename T>
static void F(T t) {
Console::WriteLine("one");
}
generic<typename T>
static void F(T t1, T t2) {
Console::WriteLine("two");
}
generic<typename T>
static void F(T t1, int t2) {
Console::WriteLine("three");
}
};
int main() {
X::F<int>(1); // 明示的。"one" を出力
X::F(1); // 推論。"one" を出力
X::F<double>(5.0, 6.0); // 明示的。"two" を出力
X::F(5.0, 6.0); // 推論。"two" を出力
X::F<double>(5.0, 3); // 明示的。"three" を出力
X::F(5.0, 3); // 推論。"three" を出力
X::F<int>(1, 2); // エラー、あいまいである
X::F(1, 2); // エラー、あいまいである
X::F<double>(1, 2); // 明示的。"three" を出力
}
][例:
interface class IX {};
ref class R : IX {};
generic<typename T>
void f(T) {}
void g(R^ hR) {
f<IX^>(hr); // T は IX に特殊化される
f(hR); // T は R であると推論される
}
]
型推論はジェネリック関数を使うのにより簡便な構文を許しており、プログラマーが冗長な型情報を指定するのを避けることを許します。
ジェネリック関数において、呼び出しの引数に対応する型が <narrow-string-literal-type> か <wide-string-literal-type> のどちらかである場合、推論された型 P は System::String^ 型です。[注記:関数テンプレートの文字列リテラルの型推論は System::String^ の代わりに文字の配列を結果とします。]さもなければ、ジェネリクス中の型推論はテンプレート(標準 C++ §14.8.2 )中の型推論のように振る舞います。
もし、ジェネリック関数がパラメータ配列と共に宣言されたら、その時、型推論は始めに正確なシグネイチャを使用している関数に対して実行されます。もし、型推論が成功し、結果の関数が適切であったら、関数は通常の形式で多重定義決定に望ましいものとなります。さもなくば、型推論はその展開された形に対して実行されます。
デリゲートのインスタンスはジェネリック関数宣言を参照して作成されます。型引数は、デリゲートがインスタンス化された時、デリゲートを通して含まれているジェネリック関数が決定された時に、利用されます。ジェネリック・デリゲートのための型引数はジェネリック関数を呼ぶための型推論と同じ振る舞いでデリゲートが呼び出されたとき、推論されます。もし、型推論が使われた場合、デリゲートのパラメータ型は推論の過程において引数型として使われます。デリゲートの返却型を推論に使いません。[例:次の例はデリゲートのインスタンス化演算に型引数を宛う二つの方法を示しています。
delegate int D(String^ s, int i);
delegate int E();
ref class X {
public:
generic<typename T>
static T F(String^ s, T t);
generic<typename T>
static T G();
};
int main() {
D^ d1 = gcnew D(X::F<int>); // okay, 型引数は明示的に与えられた
D^ d2 = gcnew D(X::F); // okay, int 型が型引数であると推論
E^ e1 = gcnew E(X::G<int>); // okay, 型引数は明示的に与えられた
E^ e2 = gcnew E(X::G); // エラー、返却型からは推論できない
}
]
非ジェネリック・デリゲート型はジェネリック関数を使ってインスタンス化することができます。ジェネリック関数を使って、生成デリゲート型のインスタンスを作成することも可能です。全ての場合において、デリゲート・インスタンスが作成される時に、型引数は与えられるか推論され、
type-argument-list(型引数リスト)はデリゲートが呼び出された時に与えられるのではありません。
31.4 拘束
ジェネリック型宣言やジェネリック関数宣言の型パラメータとして与えることが許されている型引数のセットは、一つ以上の拘束を使うことで制限することができます。そのような拘束は
constraint-clause-list(拘束項リスト)を通して指定します。
constraint-clause-list:
constraint-clause-listopt constraint-clause
constraint-clause:
where identifier : constraint-item-list
constraint-item-list:
constraint-item
constraint-item-list , constraint-item
constraint-item:
type-id
ref class
ref struct
value class
value struct
gcnew( )
各々の
constraint-clause(拘束項)は、トークン where に続いて、この
constraint-clause(拘束項)が与えるジェネリック型宣言中の型パラメータの名前となる
identifier(識別子)、続いて、コロンとその型パラメータの拘束のリストによって成り立っています。
任意のジェネリック宣言の型パラメータ毎に一つまでの
constraint-clause(拘束項)があり、
拘束項(constraint-clause)はどんな順序で列記しても構いません。
トークン where はキーワードではありません。
ジェネリック関数のためのジェネリック拘束は多重定義解決の後に検証されます。拘束は多重定義解決には影響を与えません。
[注意:value class と value struct は変換のフェイズの早期に単一トークンに置き換わるので、以下のコードは曖昧でない T の値クラス拘束を持ちます。
generic<typename T>
where T : value class
V F(T t) { ... }
返却型としてネイティブ・クラスの
elaborated-type-specifierとして使われる関数の続きの場所に value という名前の型拘束を作ることはできません。]
もし、
type-id で指定された型が ref クラスであれば、それは
クラス拘束です。クラス拘束は sealed されるべきではありません。
constraint-item-list は一つより多いクラス拘束を含むべきではありません。
もし、
type-id で指定された型がインターフェイス・クラス型であれば、それは
インターフェイス拘束です。同じインターフェイス型は与えられた
constraint-clause で一つより多く指定されるべきではありません。
もし、
type-id で指定された型がジェネリック型パラメータであれば、それは
裸の型パラメータ拘束です。同じ裸の型パラメータは与えられた
constraint-clause で一つより多く指定されるべきではありません。もし、型パラメータがそれ自身を、直接的か非直接的化のどちらかで拘束する結果となる場合、プログラムは不正です。裸の型パラメータによって指定された拘束のどれも拘束節中で与えられた他の拘束と衝突するべきではありません。例えば、拘束リストはクラス拘束と裸の型パラメータ拘束を、パラメータ拘束はそれ自身クラス拘束を持つので、持つべきではありません。
クラスやインターフェイス拘束は、生成型の一部として型や関数の宣言に関連づけされている型パラメータのどれにでも含むことができ、宣言されている型を含むことができます。
型パラメータ拘束として指定された任意のクラス型やインターフェイス型は、少なくとも、宣言されているジェネリック型やジェネリック関数と同じアクセス可能性であるべきでしょう。
もし、
type-id で指定された型がそれ以外の何かだったら、プログラムは不正です。
[例:次に拘束の例を示します。
generic<typename T>
interface class IComparable {
int CompareTp(T value);
};
generic<typename T>
interface class IKeyProvider {
T GetKey();
};
generic<typename T>
where T : IPrintable
ref class Printer { ... };
generic<typename T>
where T : IComparable<T>
ref class SortedList { ... };
generic<typename K, typename V>
where K : IComparable<K>
where V : IPrintable, IKeyProvider<K>
ref class Dictionary { ... };
]
もし、型パラメータがそれに関連づけられた拘束を一つも持っていない場合、その型パラメータは暗黙的に System::Object に拘束されていることになります。
[注意:この振る舞いで拘束された型パラメータを持つことによって、あなたがジェネリックの本文中でその型にどうすることができるのかを厳しく制限します。]
ジェネリック
constraint-item は
elaborated-type-specifier を持つべきではありません。
ジェネリック型パラメータの拘束は順序づけや多重定義解決に何の影響も持ち得ません。関数テンプレートの部分指定のルールはジェネリック関数に当てはまります。
関数テンプレートを使って明示的にジェネリック関数の特殊化を実現しようとするプログラムは不正です。
31.4.1 拘束を満たす
生成型やジェネリック関数が参照された時にはいつでも、それらのジェネリック型やジェネリック関数で宣言されている型パラメータ拘束について与えられた型引数は検証が行われます。各々の where 節毎に名前付けられた型パラメータに対応する型引数 A は、拘束毎に次のようなチェックされることになります。
もしも、与えられた型引数が型パラメータの拘束のどれか一つ以上満たしていないようなジェネリック型を含んでいる場合、プログラムは不正です。
型パラメータは継承されないため、拘束も同様に決して継承されません。[例:以下のコードでは、T が基底クラス B<T> に課せられた拘束を満たしているため、D はその型パラメータ T の拘束に指定されなければなりません。対照的に、クラス E では、List<T> が任意の T に対して IEnumerable を実装しているので、拘束を満たす必要はありません。
generic<typename T>
where T : IEnumerable
ref class B { ... };
generic<typename T>
where T : IEnumerable
ref class D : B<T> { ... };
generic<typename T>
ref class E : B<List<T>^> { ... };
]
31.4.2 型パラメータのメンバ検索
テンプレートは型引数によって型パラメータが置き換えられるまで、型パラメータによる検索の実行を待機します。ジェネリクスは特殊化の時点でよりもむしろ、ジェネリックを定義する時点で検索を実行します。型パラメータ T として与えられた型のメンバ検索の結果は、いずれにせよ、T に指定された拘束に依存します。名前検索は以下の場合のどれか一つによって指定された型として、ジェネリック型パラメータ T の型を置き換えます。
- もし、T が拘束を持たない、もしくは、コンストラクタ拘束のみを持つのであれば、T は System::Object に置き換わります。もし、名前検索がそのコンストラクタを選ぶ場合には、その型は System::Activator::CreateInstance を呼ぶことによって生成されます。
- もし、T がなんらかの値クラス拘束であれば、値クラス V は以下の文字列に同期されます。V は 名前検索のために T を置き換えます。
- もし、T が何らかのインターフェイス拘束を持っていれば、V はそれぞれのインターフェイスの実装を提供します。もし、名前検索と多重定義解決がそれらの関数の一つを選ぶ場合、拘束は同期した関数によって実装されたインターフェイス関数に出会います。
- もし、T がなんらかの ref型クラス拘束を持っているのであれば、ref 型クラス R は以下の記述によって同期されます。R は名前検索のために T を置き換えます。
- もし、T がなんらかのインターフェイス拘束を持っていれば、R はそれぞれのインターフェイスの実装を提供します。もし、名前検索と多重定義解決がこれらの関数のどれか一つを選択する場合には、拘束は同期した関数によって実装されたインターフェイス関数に出会います。
- もし、T がコンストラクタ拘束を持っていれば、R はパラメータ無しの public コンストラクタを提供します。もし、名前検索がこの同期したコンストラクタを選択すれば、その型は System::Activator::CreateInstance を呼ぶことによって生成されます。
- もし、T がある基底クラス拘束 B を持っており、B は T の他の全ての拘束を満たすのであれば、B は T を置き換えます。そうでなければ、ref 型クラス R は直ちに B から派生し、次の記述通りに同期されます。R は名前検索のために T を置き換えます。
- もし、T がなんらかのインターフェイス拘束を持っていれば、R は B からは制することによってすでに満たされていないインターフェイス関数のそれぞれの実装を提供します。もし、名前検索と多重定義解決が同期された関数の一つを選択すれば、拘束は同期された関数によって実装されたインターフェイス関数に出会います。[注意:もし、基底クラス拘束とインターフェイス拘束が同じ関数シグネイチャを持っていれば、そのような、インターフェイス関数を実装することができる基底クラスの関数であり、ジェネリック型パラメータをとおした関数呼び出しは基底クラスを通して行われます。]
- もし、T がコンストラクタ拘束を持っていれば、R はパラメータを持たない public コンストラクタを提供します。もし、名前検索が同期したコンストラクタを選んだ場合、その型は System::Activator::CreateInstance を呼ぶことによって生成されます。
- もし、T が ref 型クラス拘束も、値クラス拘束も、基底クラス拘束も持っていなかった場合、ref 型でもあり、値型でもある RV 型クラスは次の記述通りに同期されます。(そのようなハイブリッド・クラスは ref 型クラスと値クラスの双方を使って二度名前検索を行うことによって同期することができ、その結果をマッチすることで保証することができます)
- もし、T がなんらかのインターフェイス拘束を持っていれば、RV は各々のインターフェイスに実装を提供します。もし、名前検索と多重定義解決がこれらの関数の一つを選択する場合、その拘束は同期された関数によって実装されたインターフェイス関数に出会います。
- もし、T がコンストラクタ拘束を持っていれば、その RV によって表現される ref 型クラスはパラメータのない public コンストラクタを提供します。もし、名前検索がこの同期されたコンストラクタを選択する場合、その型は System::Activator::CreateInstance を呼ぶことで生成されます。
[例:以下のようなコードについて考えると、
interface class IMethod {
void F();
};
ref struct R : IMethod {
virtual void G() = IMethod::F {
System::Console::WriteLine("R::G");
}
void F() {
System::Console::WriteLine("R::F");
}
};
generic<typename X>
where X : IMethod
void G1(X x) {
x->F();
}
generic<typename X>
where X : R, IMethod
void G2(X x) {
x->F();
}
template<typename x>
void T(X x) {
x->F();
}
int main() {
R^ r = gcnew R;
G1(r);
G2(r);
T(r);
}
プログラムは以下のような出力を表示します。
R::G
R::F
R::F
G1 の型パラメータはただ一つのインターフェイス拘束を持っています。そのため、同期した型が拘束を実装した関数 F について作られます。故に、G1 の本体中の F の呼び出しは、インターフェイスを通しています。G2 の型パラメータは基底クラス拘束とインターフェイス拘束の両方を持っています。基底クラスはすでにインターフェイスを実装しているので、故に、名前検索の目標は、G2 の本体中の R で置き換えられます。]
31.4.3 型パラメータとboxing
値クラスが System::Object から継承された virtual メソッドを上書きする時(例えば、Equalsや GetHashCode、 ToString とか)、値クラスのインスタンスを通した仮想関数の呼び出しはボックス化を発生させません。これは値クラスが型パラメータとして使われ、型パラメータ型のインスタンスを通して呼び出しが発生した時ですら、真となります。
ボックス化は、型パラメータに拘束されたメンバにアクセスしている時、暗黙のうちに発生したりはしません。例えば、値を変更される時に使われる Increment という関数を含んだインターフェイス ICounter を仮定します。もし、ICounter が拘束として使われ、Increment 関数の実装が Increment を呼び出した変数への参照で呼び出されたとしても、ボックス化されたコピーを決して作成しません。
31.4.4 型パラメータを含んだ型変換
型パラメータ T に許されている変換は T に指定されている拘束に依存しています。
ジェネリック型やジェネリック関数がクラスとインターフェースの双方の拘束を持っているため、クラス拘束に定義されている型変換は常にインターフェイス拘束の型変換に比べて優先されます。