8.言語概説
この章は情報文です。
この仕様は標準C++の包含セットです。
この章ではこの仕様の基本的な特徴を記述します。
後の章で詳しいルールや例外を記述するので、この節ではわかりやすく簡潔に説明するため、あえて完全さを犠牲にして端折っています。
この章は読者に早くプログラムを書くこと、先の章を読み進めることに熱中してもらうために、この言語を紹介することに専念しています。
8.1 さぁ、始めよう
伝統的な"hello, world"プログラムは次のように記述します。
int main() {
System::Console::WriteLine("hello, world");
}
C++/CLI プログラムのソースコードは典型的に hello.cpp のように、
.cpp の拡張子を持った一つ以上のテキストファイル中に蓄えられます。
コマンド行コンパイラ(例えば、cl で)を使って、このようにしてコマンド行で先ほどのプログラムのコンパイルができます。
cl hello.cpp
それは、hello.exe という名前のアプリケーションを作成します。このアプリケーションを動かすと、アプリケーションは出力を生成します。
hello, world
WriteLine 関数は自動的に最後に改行を追加します。
CLI ライブラリは多くの名前空間に組織化されており、普段もっとも System が使用されます。
その名前空間は
Console と呼ばれる、コンソール I/O を実行する関数ファミリーを提供する ref クラスを含んでいます。
その関数の一つが
WriteLine であり、文字列を与えることで、コンソールに末尾に改行を加えてその文字列を出力します。
(ここからの例では System 名前空間については
using-declaration ( using 宣言)がされているものと仮定します)
8.2 型
値クラス型とハンドル型クラスとは、ハンドル型クラスの変数はオブジェクトへのハンドルを保持しているのに対して、値クラスの変数では直接そのデータを保持しているという点で異なっています。ハンドル型クラスでは、二つの変数は共に同一オブジェクトへの参照を表すことができます。そして、そのために、他の変数によって参照された、ある変数の示すオブジェクトに影響を及ぼすような操作が可能です。値クラスでは、変数は互いに自分自身のデータのコピーを持っているので、互いに影響を及ぼすような操作はできません。
例としては、
ref class Class1 {
public:
int value;
Class1()
{
value = 0;
}
};
int main()
{
int val1 = 0;
int val2 = val1;
val2 = 123;
Class1^ ref1 = gcnew Class1;
Class1^ ref2 = ref1;
ref2->value = 123;
Console::WriteLine("Values: {0}, {1}", val1, val2);
Console::WriteLine("Refs: {0}, {1}", ref1->value, ref2->value);
}
この例ではその違いを示しています。生成された出力は、
Values: 0, 123
Refs: 123, 123
双方のローカル変数が(int型の)値クラスであり、それぞれの値クラスのローカル変数はそれぞれがデータの保持領域を持っているので、ローカル変数 val1 への代入はローカル変数 val2 に影響しません。対照的に、ref2->value = 123; の代入は ref1 と ref2 の双方が参照しているオブジェクトに影響を与えます。
行、
Console::WriteLine("Values: {0}, {1}", val1, val2);
Console::WriteLine("Refs: {0}, {1}", ref1->value, ref2->value);
は、Console::WriteLine の文字列整形の振る舞いが、実際に複数の引数を持っており、いくつかデモを見せているので、ちょっと説明する価値があります。最初の引数は文字列で、{0} や {1} といった、順序化された出力位置を含んでいます。それぞれの出力位置ごとに、{0} には2番目の引数が、{1} には3番目の引数が、などといったように、続く引数が対応しています。コンソールに出力が送られる前、それぞれの出力位置は引数に対応した整形済みの値に置き換えられます。
開発者は enum と value class 定義を通して新しい値クラスを定義できます。
以下のコードはそれぞれの型定義の例を示しています。後の節で型定義の詳細を記述します。
public enum class Color {
Red, Blue, Green
};
public value struct Point {
int x, y;
};
public interface class IBase {
void F();
};
public interface class IDerived : IBase {
void G();
};
public ref class A {
protected:
virtual void H() {
Console::WriteLine("A.H");
}
};
public ref class B : A, IDerived {
public:
void F() {
Console::WriteLine("B::F, IDerived::F の実装");
}
void G() {
Console::WriteLine("B::G, IDerived::G の実装");
}
virtual protected void H() override {
Console::WriteLine(B::H, A::H の関数上書き(オーバーライド)");
}
};
public delegate void MyDelegate();
上の Color や Point、IBase のような型は、他の型の内側で定義されていないので(つまり、これらは上層型です)、public、もしくは、private の型の視覚化指定子を持つことができます。この文脈での public の使用は、アセンブリの外部からその型が見えることを指し示しています。反対に、private はアセンブリの外側からその型が見ることができないことを示します。デフォルトの上層型の視覚化指定子は private です。
8.2.1 基本型とCLI型
それぞれの基本型は、実装が提供する対応した値クラス型を持っています。その対応は実装定義です。例えば、ある実装では、int は System::Int32 を対応する型に指定しているかもしれませんし、また、別の実装では System::Int64 に対応しているかもしれません。対応する CLI 型名は特定の CLI プラットフォーム型を指し示すことになるため、キーワード型名称を使うことは通常の標準 C++ であるという意味を持ちます。
[例:int は実装定義として"自然な"整数を規定しています。それに比べて、Int32 は任意のCLIプラットフォーム上で 32 ビットに厳密に指定します。]
下のテーブルは
ある実装における基本型とそれに対応するCLI提供型のリストです。
一貫性のため、この標準中の例では"実装定義"とあえて言及しない限り、このテーブルの値を扱います。
| 型 | 記述 | CLI値型 |
| bool | Boolean型。bool値は true もしくは、false | System::Boolean |
| char | 8-bit 符号化/非符号化整数値 | System::SByte もしくは System::Byte ( IsSignUnspecifiedByte のオプションによる ) |
| signed char | 8-bit 符号化整数値 | System::SByte |
| unsigned char | 8-bit 非符号化整数値 | System::Byte |
| short | 16-bit 符号化整数値 | System::Int16 |
| unsigned short | 16-bit 非符号化整数値 | System::UInt16 |
| int | 32-bit 符号化整数値 | System::Int32 |
| unsigned int | 32-bit 非符号化整数値 | System::UInt32 |
| long | 32-bit 符号化整数値 | System::Int32 ( IsLong のオプションによる ) |
| unsigned long | 32-bit 非符号化整数値 | System::UInt32 ( IsLong のオプションによる ) |
| long long int | 64-bit 符号化整数値 | System::Int64 |
| unsigned long long int | 64-bit 非符号化整数値 | System::UInt64 |
| float | 単精度浮動小数点値 | System::Single |
| double | 倍精度浮動小数点値 | System::Double |
| long double | 長倍精度浮動小数点値 | System::Double ( IsLong オプションによる ) |
| wchar_t | 16-bit ユニコード・コード値 | System::Char |
基本型ではありませんが、もう3つの型が CLI ライブラリによって提供されていることを伝えておきます。それらは、
- System::Object, 全ての値型とハンドル型の究極的な基本となる型
- System::String,ユニコード・コード値の順序型
- System::Decimal,28厳密桁数の精密10進型
C++/CLI にはこれらに対応するキーワードはありません。
8.2.2 変換
多くの新しい種類の型変換が定義されています。これらにはハンドル型やパラメータ配列の変換等々が含まれています。
8.2.3 CLI 配列型
CLI 配列型は、ネイティブな配列(標準 C++ §8.3.4 )と異なり、CLI ヒープ上に領域確保され、一つ以上の位階(ランク)を持つことができます。位階はそれぞれの配列要素ごとに関連する指示数によって決定されます。CLI 配列の位階はまた配列の
次元とも呼びます。
1の位階を持つ配列は
1次元配列とも呼ばれ、1より多い位階を持つ CLI 配列のことを
多次元 CLI 配列と呼びます。
この標準を通して、
CLI 配列という言葉は CLI 上の配列を意味して使います。
C++ の配列は、あえて区別する必要がある場合には
ネイティブ配列と、または、単に
配列と呼称します。
CLI 配列型は以下のような宣言を持つ組み込み式の擬似テンプレートを使った ref クラスとして宣言されます。
namespace cli {
template<typename T, int rank = 1>
ref class array : Array {
};
}
この擬似テンプレートを使う例です。
int main() {
array<int>^ arr1D = gcnew array<int>(4) { 10, 42, 30, 12};
Console::Write("{0} 個の要素があります : ", arr1D->Length);
for each (int i in arr1D) {
Console::Write("{0,3}", i);
}
Console::WriteLine();
array<int, 3&t;^ arr3D = gcnew array<int, 3>(10, 20, 30);
}
生成される出力です。
4 個の要素があります : 10 42 30 12
ハンドル arr1D は int の任意長の一次元配列として作成されます。
それは現在、四つの int 要素を含む、一つの配列を参照します。
読み込みのみプロパティ Array::Length は要素数を含んでいます。
ハンドル arr3D は int の任意の 3 次元配列を参照することができます。
それは現在、10×20×30のサイズの、int のデフォルト値、つまり 0、を要素全てに詰め込んだ配列一つを参照します。
8.2.4 統一型システム
C++/CLI は
統一型システムを提供します。
全ての値型、ハンドル型は System::Object 型から派生します。
任意の値から、int のような基本型からですら、インスタンスの関数を呼び出すことができます。
例としては、
int main() {
Console::WriteLine( (3).ToString() );
}
これは整数文字列からその型 System::Int32 のインスタンス関数 ToString を呼んでいます。
結果として 文字列
3が出力されます。
(文字 3 の周囲を囲んでいる括弧は冗長のように見えますが、それらは文字列
3.でなく、
3と
.を取り出すために必要になります。)
例、
int main() {
int i = 123;
Object^ o = i; // ボックス化
int j = static_cast<int> (o); // 非ボックス化
}
は、より興味深いです。ある int 値は System::Object^ 型に変換することができ、また、int 型に変換し直すことができます。
この例は
ボックス化と
非ボックス化の双方を示しています。
値クラスの変数をハンドル型に変換する必要があるとき、System::Object
box はその値を保持するために領域確保され、そして、値がその領域にコピーされます。
非ボックス化はその反対です。
System::Object box ハンドルがその元の値クラス型にキャストし直される時、box から取り出されて、適切なデータ保管位置に値はコピーされます。
この型システムの統一は不要なオーバーヘッドなしで、オブジェクト性の利点を値クラスに享受させてくれます。
プログラムには CLI オブジェクトのように振る舞う int 値など必要ありません。
int 値は単に32-bitの値です。
プログラムには、CLI オブジェクトのように振る舞う int 値こそが必要なのです。
この許容性は必要に応じて提供されます。
値クラス型のインスタンスを CLI オブジェクトのように扱える能力は、多くの言語に存在する値クラスと ref クラスの間のギャップの橋渡しをします。
例えば、Object^ 型を受け取り、返す Stack クラスは Push や Pop 関数を提供できます。
public ref class Stack {
public:
Object ^Pop() {...}
void Push(object ^o) {...}
};
C++/CLI が統一された型システムを持っているおかげで、Stack クラスは、int のような値クラス型を含む任意の型の要素に対して使用することができるのです。
8.2.5 ポインタ、ハンドル、そして、null
標準C++ はポインタ型とnullポインタ定数をサポートしています。
C++/CLI ではそこにハンドル型と null 値を追加します。ハンドル型の統合を助けるため、そして、統一 null 値を実現するために、C++/CLI では nullptr キーワードを定義しています。
このキーワードはnull型を持つリテラルを表現しています。nullptr は
null 値定数 とも呼ばれます。
(null 型のインスタンスを作ったりできません。このキーワードを通して null 値定数を手に入れることが唯一の方法です。)
null ポインタ定数の定義(標準C++ ではコンパイル時に 0 に評価することが要求されています)は nullptr を含むよう拡張されています。
null 値定数は暗黙のうちに任意のポインタ型やハンドル型に、それそれの場合に対応した
null ポインタ値、ないし、null 値におのおの変換されます。
これによって nullptr が関係、等価性、条件式、代入演算、などなどに利用されることを許しています。
Object ^ obj1 = nullptr; // ハンドル obj1 は null 値を持つ
String ^ str1 = nullptr; // ハンドル str1 は null 値を持つ
if ( obj1 == 0 ); // false ( 0はボックス化され、ハンドルが比較される)
if ( obj1 == 0L ); // false (上に同じ)
if ( obj1 == nullptr ); // true
char *pc1 = nullptr; // pc1 は null ポインタ値となる
if ( pc1 == 0 ); // true ( 0 は null ポインタ値だから)
if ( pc1 == 0L ); // true (上に同じ)
if ( pc1 == nullptr ); // true nullptr は null 値定数なので
int n1 = 0;
n1 = nullptr; // error int 値への変換は用意されていない
if ( n1 == 0 ); // true 整数値比較が実行された
if ( n1 == 0L ); // true (上に同じ)
if ( n1 == nullptr ); // error int 値への変換は用意されていない
if ( nullptr ); // error
if ( nullptr == 0 ); // error int 値への変換は用意されていない
if ( nullptr == 0L ); // error int 値への変換は用意されていない
nullptr = 0; // error nullptr は左辺値ではない
nullptr + 2; // error nullptr は算術計算の一部にならない
Object ^ obj2 = 0; // obj2 はボックス化された0を保持する
Object ^ obj3 = 0L; // obj3 上に同じ
String ^ str2 = 0; // error int型からString^型への変換は存在しない
String ^ str3 = 0L; // 上に同じ
char * pc2 = 0; // pc2 はnullポインタ値である
char * pc3 = 0L; // pc3 上に同じ
Object ^ obj4 = expr ? nullptr : nullptr; // obj4 はnull値である
Object ^ obj5 = expr ? 0 : nullptr; // error 混合式はない
char * pc4 = expr ? nullptr : nullptr; // pc4 はnullポインタ値
char * pc5 = expr ? 0 : nullptr; // error 混合式はない
int n2 = expr ? nullptr : nullptr; // error int 型への変換が存在しない
int n3 = expr ? 0 : nullptr; // error 混合式は存在しない
sizeof(nullptr); // error null型はサイズを持たない
typeid(nullptr); // error
throw nullptr; // error
void f(Object^); // 1
void f(String^); // 2
void f(char *); // 3
void f(int); // 4
f(nullptr); // error あいまいである( 1, 2, 3 では可能 )
f(0); // f(int) を呼び出す
void g(Object^, Object^); // 1
void g(Object^, char *); // 2
void g(Object^, int); // 3
g(nullptr, nullptr); // error あいまいである( 1, 2 では可能 )
g(nullptr, 0); // g(Object^, int) を呼び出す
g(0, nullptr); // error あいまいである( 1, 2 では可能 )
void h(Object^, int);
void h(char*, Object^);
h(nullptr, nullptr); // h(char*, Object^) を呼び出す
h(nullptr, 2); // h(Object^, int) を呼び出す
template<typename T> void k(T t);
k(0); // k, T = int に特殊化される
k(nullptr); // error null型はインスタンス化できない
k((Object^)nullptr); // k, T = Object^ に特殊化される
k<int*>(nullptr); // k, T = int* に特殊化される
ネイティブ・ヒープ上に領域確保されたオブジェクトは移動しないので、それらのオブジェクトを指し示すポインタや参照はオブジェクト配置を追跡する必要がありません。
しかしながら、CLI ヒープ上のオブジェクトは移動しうるので、オブジェクトの配置位置を追跡する必要があります。
そのようなオブジェクトを扱うには、ネイティブ・ポインタや参照では充分ではありません。
CLI ヒープ上のオブジェクトを追跡するために、C++/CLI はハンドル( 接尾子
^ を使用します)と追跡参照(接尾子
% を使用します)を定義します。
N* hn = new N; // ネイティブ・ヒープ上に領域確保
N& rn = *hn; // ネイティブ・オブジェクトに通常の参照を接続
R^ hr = gcnew R; // CLIヒープ上に領域確保
R% rr = *hr; // gc-lvalue に追跡参照を接続
一般に、& が * に対応するように、% は ^ に対応します。
ちょうど標準 C++ が前置演算子 & を持っているように、C++/CLI は前置演算子 % を提供します。
&t が T* もしくはinterior_ptr<T>(下を見よ)を明け渡すように、%t は T^ を譲渡します。
rvalue と lvalue は、次のようなルールに従うことによって、標準C++と同様の意味を持ち続けています。
- T* の形で宣言された項目は、Tへのネイティブ・ポインタを表し、lvalue を指し示す。
- T* の形で宣言された項目にあてがわれた前置演算子*はT*の参照を外し、lvalueを与える。
- T& の形で宣言された項目は、Tへのネイティブ参照を表し、lvalue自身である。
- &lvalue 表現は T* を与える
- %lvalue 表現は T^ を与える
gc-lvalue は CLI ヒープ上のオブジェクトを参照する、もしくは、そのようなオブジェクトに含まれた値メンバへの演算です。gc-lvalue には以下のルールが適用されます。
- "T 型のcv-qualified lvalue" から "T 型のcv-qualified gc-lvalue" への標準的な型変換が存在する。そして、"T 型の cv-qualified gc-lvalue"から"T 型の cv-qualified rvalue"への標準的な型変換も存在する。
- T^ の形で宣言された項目は、T のハンドルであり、gc-lvalue を指し示す。
- T^ の型で宣言された項目に与えられた前置演算子 * は、T^ の参照を外し、gc-lvalueを与える。
- T% の型で宣言された項目は、T の追跡参照であり、gc-lvalue そのものである。
- &gc-lvalue 表現は interior_ptr を与える(下記参照)。
- %gc-lvalue 表現は T^ を与える。
ガベージ・コレクタは CLI ヒープ上のオブジェクトが移動することを許しています。そのようなオブジェクトに正確に対応したポインタのために、ランタイムはオブジェクトの新しい位置へポインタを更新する必要があります。内部ポインタ(interior_ptrを使って定義された)はこの振る舞いによって更新されるポインタです。
8.3 パラメータ
パラメータ配列は省略で終了するパラメータのリストの型安全な代換え物です。
パラメータ配列は
...句点に先導され CLI 配列型が続く形で宣言されます。
与えられた関数には、パラメータ配列はただ一つしか許されず、常に最後のパラメータとして指定されるべきです。
パラメータ配列の型は常に1次元の CLI 配列型です。
呼び出し側はこの CLI 配列型の引数が一つであろうと、任意の個数の CLI 配列型の要素を引数に持とうと、通すことができます。例えば、例
void F(... array<int>^ args) {
Console::WriteLine("引数の # : {0}", args->Length);
for ( int i=0; i<args->Length; i++ )
Console::WriteLine("\targs[{0}] = {1}", i, args[i]);
}
int main() {
F();
F(1);
F(1, 2);
F(1, 2, 3);
F( gcnew array<int> { 1, 2, 3, 4 } );
}
は、関数 F が int 型の可変引数を持つことを示しており、この関数はいくつかの呼び出し方式が取れることを表しています。
出力です。
引数の # : 0
引数の # : 1
args[0] = 1
引数の # : 2
args[0] = 1
args[1] = 2
引数の # : 3
args[0] = 1
args[1] = 2
args[2] = 3
引数の # : 4
args[0] = 1
args[1] = 2
args[2] = 3
args[3] = 4
System::Object^ 型の CLI 配列型を配列パラメータとして宣言することで、パラメータは雑多な型の引数を取ることができます。例としては、
void G(... array<Object^>^args) { ... }
G(10, "Hello", 1.23, 'X'); // 引数 1, 3, 4 はボックス化される
Console クラスの関数 WriteLine を使う多くの例がこのドキュメント中に示されています。その引数の置き換えはこのパラメータ配列による振る舞いです。例を挙げると、
int a = 1, b = 2;
Console::WriteLine("a = {0}, b = {1}", a, b);
これはパラメータ配列を利用して実現されています。
Console クラスは一般的な状況で少ない数の引数を受け取るように、いくつものWriteLine 関数のオーバーロード版を提供しており、次のようなパラメータ配列を使った一般化されたバージョンが提供されています。
namespace System {
public ref class Object {...};
public ref class String {...};
public ref class Console {
public:
static void WriteLine(String^ s) {...}
static void WriteLine(String^ s, Object^ a) {...}
static void WriteLine(String^ s, Object^ a, Object^ b) {...}
static void WriteLine(String^ s, Object^ a, Object^ b, Object^ c) {...}
...
static void WriteLine(String^ s, ... array<Object^>^ args) {...}
};
}
CLI ライブラリ仕様は C# の構文でライブラリの関数を示しています。その場合、C# キーワード
params がパラメータ配列を表しています。
例えば、上述の最後の WriteLine 関数宣言は C# の構文で書き直すと、次のようになります。
public static void WriteLine(string s, params object[] args)
8.4 自動メモリ管理
例、
public ref class Stack {
public:
Stack() {
first = nullptr;
}
property bool Empty {
bool get() {
return ( first == nullptr );
}
}
Object^ Pop() {
if ( first == nullptr )
throw gcnew Exception("空のスタックからは Pop できません。");
else {
Object^ temp = first->Value;
first = first->Next;
return temp;
}
}
void Push(Object^ o) {
first = gcnew Node(o, first);
}
ref struct Node {
Node^ Next;
Object^ Value;
Node(Object^ value) : Node(value, nullptr) {}
Node(Object^ value, Node^ next) {
Next = next;
Value = value;
}
};
private:
Node^ first;
};
これは、Stack クラスが Node インスタンスのリンクド・リストとして実装されている例を示しています。
Node インスタンスは Push 関数内で生成され、いらなくなった時点でガベージ・コレクトされます。
Node インスタンスはそのインスタンスにもうアクセスするコードが存在しなくなった時、ガベージ・コレクトされるにふさわしくなります。
例えば、Stack からアイテムが削除されたとき、関連する Node はガベージ・コレクトが実行されるべきです。
例として、
int main() {
Stack^ s = gcnew Stack();
for ( int i = 0; i <10; i++ )
s->Push(i);
s = nullptr;
}
これは Stack クラスを使うコードを提示しています。Stack クラスは生成され、10の要素で初期化され、そして、スタックへのハンドルに nullptr 値が代入されます。
ひとたび、変数 s に null 値が代入されると、Stack と関連した 10 のNodeインスタンスはガベージ・コレクトされるにふさわしくなります。
ガベージ・コレクションは直ちに消去の許可を下しますが、それは直ちに消去を実行することを要請していません。
C++/CLI におけるガベージ・コレクタは CLI ヒープ上のオブジェクトの移動を伴って動作しますが、この働きは大部分の C++/CLI 開発者には見えません。
開発者にとって一般的にメモリは自動的に管理されているものですが、時々、適切な粒度での制御やちょっとしたパフォーマンスが必要になる場合があるので、C++/CLI では一時的にそのオブジェクトがガベージ・コレクタによって移動しないよう妨げる、オブジェクトを
pin する機能を提供しています。例としては、
void f(int *p) { *p = 100; }
int main() {
array<int>^ arr = gcnew array<int>(100);
pin_ptr<int> pinp = &arr[0]; // arr の場所を pin する。
f(pinp); // arr[0] の値を変更
}
8.5 演算
C++/CLI では演算子について標準 C++ を拡張します。例えば、
- デリゲートの追加は delegate によってカプセル化された関数呼び出しに関数呼び出し演算子の利用を要請しています。
- typeid の新しい使い方が追加されています。例えば、Int32::typeid は CLI 型 Int32 を示す System::Type 型の CLI オブジェクトへのハンドルを結果とします。
- ハンドル型に対応してキャスト演算子が拡張されました。
- safe_cast 演算子が追加されました。
- 新しい演算子 gcnew が追加されました。これは CLI ヒープからメモリを領域確保します。
- 二項演算子 + と - は delegate への追加と削除に期待通りに動作するよう拡張されました。
- 単純代入が左項としてイベントやプロパティへの代入に対応するよう拡張されました。
- 複合代入演算子は対応する二項演算子から合成されます。(§19.7.4 )
8.6 命令文
新しい命令文
for each が追加されました。この命令文はコレクションの要素を順次取得してゆき、そのコレクションの各々の要素にブロック文を実行します。例えば、
void display(array<int> ^args) {
for each ( int i in args )
Console::WriteLine(i);
}
コレクション型と呼ぶ型は System::Collection を実装したものです。IEnumerable インターフェイス、もしくは、いくつかの多くの基準に沿って
コレクション・パターンを実装しています。
8.7 デリゲート
デリゲートは典型的に標準 C++ ライブラリで関数アダプタとして標準 C++ プログラマに提示されているシナリオを可能にしています。
デリゲートの定義は
System::Delegate クラスから派生したクラスであると暗黙のうちに定義されます。デリゲートのインスタンスは一つ以上の関数を
呼び出しリストにカプセル化しており、その各メンバー毎に
呼び出し可能エンティティと参照されます。インスタンス関数のための呼び出し可能エンティティは、インスタンスとそのインスタンスのメンバ関数となっています。静的関数ないし、グローバル、名前空間スコープ関数には呼び出し可能エンティティは単にそのメンバ、ないし、グローバル、名前空間スコープの関数です。デリゲートのインスタンスに引数の適切なセットを与えることで、デリゲートのインスタンスが持つ全ての呼び出し可能エンティティを、引数のセット与えたものとして呼び出すことができます。
次の例を考えます。
delegate void MyFunction(int value); // デリゲート型の定義
public ref struct A {
static void F(int i) ( Console::WriteLine("F:{0}", i); }
};
public ref struct B {
void G(int i) { Console::WriteLine("G:{0}", i); }
};
静的関数 A::F と インスタンス関数 B::G はともに MyFunction と同じパラメータ型とリターン型を持っているので、これらは MyFunction デリゲート型にカプセル化することができます。双方ともに public であることを注視してください。これらのアクセス可能性は MyFunction との相互関係には無関係です。これらの関数はまた、プログラマから見て適切であれば、クラスが同一であろうとなかろうと関係なく定義することができます。
int main() {
MyFunction^ d; // デリゲート型の参照を作成
d = gcnew MyFunction(&A::F); // 呼び出しリストは A::F
d(10);
B^ b = gcnew B;
d += gcnew MyFunction(b, &B::G); // 呼び出しリストは A::F, B::G
d(20);
d += gcnew MyFunction(&A::F); // 呼び出しリストは A::F, B::G, A::F
d(30);
d -= gcnew MyFunction(b, &B::G); // 呼び出しリストは A::F, A::F
d(40);
}
F:10
F:20
G:20
F:30
G:30
F:30
F:40
F:40
非静的メンバ関数に結びつけられているデリゲートのコンストラクタは二つの引数を必要とします。最初は ref 型クラスのインスタンスのハンドルで、二番目が ref 型クラス中の非静的メンバ関数の(メンバへのポインタの構文を使っての)アドレスです。静的関数に結びつけられているデリゲートのコンストラクタではたった一つ、静的メンバ関数のアドレスだけを引数として必要としています。
互換な二つのデリゲートの呼び出しリストは、上述してあるように、+= 演算子を通して結びつけることができます。また、呼び出し可能なエンティティは、上述したとおり、-= 演算子によって呼び出しリストから削除できます。しかし、呼び出しリストは一度作成されると変更することはできません。これらの演算子は特別に、新しい呼び出しリストを作成しています。
一度、デリゲート・インスタンスが初期化されたら、それらのカプセル化された関数は、関数個々の名前の代わりにデリゲートのインスタンスの名前が使われるという点を除いて、直接呼び出されたかのように(それらのデリゲートが呼び出しリストに追加されたのと同じ順序で)、非直接的に関数呼び出しをすることができます。デリゲート呼び出しの戻り値は(もし、あるなら)、デリゲート呼び出しリストの最後の関数の戻り値です。もし、デリゲートのインスタンスが null 値で
カプセル化されているはずの関数呼び出しがなされた場合には、NullReferenceException 型の例外を結果とします。
8.8 ネイティブ・クラスとrefクラス
8.8.1 リテラル・フィールド
リテラル・フィールドはコンパイル時定数右辺値を示します。リテラル・フィールドの値は、それらが前もって定義されている範囲に置いて、同じプログラム中の他のリテラル・フィールドの値に依存することが許されます。
例えば、
ref class X {
literal int A = 1;
public:
literan int B = A + 1;
};
ref class Y {
public:
literal double C = X::B * 5.6;
};
これらは、二つのクラスを示しており、その間に、三つのリテラル・フィールドが定義されていて、そのうち二つは public であり、もう一つは private です。
リテラル・フィールドは静的メンバのようにアクセスしますが、リテラル・フィールドは静的ではなく、その定義は static キーワードを受け入れたり必要だったりしません。リテラル・フィールドはクラスを通してアクセスすることができます。このように。
int main() {
cout << "B = " << X::B << "\n";
cout << "C = " << Y::C << "\n";
}
これらは次の出力を生成します。
B = 2
C = 11.2
リテラル・フィールドはref、値、そして、インターフェイス・クラスに置いてのみ、許されています。
8.8.2 initonly フィールド
initonly 識別子は、インスタンス・コンストラクタの本文中と、静的コンストラクタと、
ctor-initilizer の中でのみ左辺値のフィールドとして宣言され、それ以降は右辺値です。その様なフィールドを
initonly フィールド と呼びます。例としては、
public ref class Data {
initonly static double coefficient1;
initonly static double coefficient2;
static Data() {
// いくつかのソースから coefficient の値を読み込む
coefficient1 = ...; // ok
coefficient2 = ...; // ok
}
public:
static void F() {
coefficient1 = ...; // error
coefficient2 = ...; // error
}
};
initonly フィールドの代入はその定義の部分として、もしくは、その同じクラスのコンストラクタや静的コンストラクタでのみ発生します。(静的 initonly フィールドは静的コンストラクタ内でのみ代入することができます。そして、非静的 initonly フィールドはインスタンスのコンストラクタ中でのみ代入することができます。)
initonly フィールドは ref クラス、値クラスでのみ許されています。
8.8.3 関数
CLI クラス型中のメンバ関数は標準 C++ と同じ様に定義され、利用されます。しかし、C++/CLI はその努力によっていくつかの違いを持っています。例えば、
- const と volatile 修飾子はインスタンス・メンバ関数には許されません。
- 関数修飾子 override とオーバーライド(上書き)指定子はオーバーライド(上書き)を明示的に指示し、名前付けオーバーライド(上書き)(§8.8.10.1 ) の能力を提供します。
- 仮想メンバ関数にsealedと印を付けることで、派生クラスにその関数をオーバーライド(上書き)することを禁止します。
- 関数修飾子 new は同名で、同じパラメータリストを持ち、同じ識別子を持つ基底クラスの関数を隠して、その関数を許します。そのような隠された関数はどんな基底クラスの関数も、virtual宣言された関数ですらも、オーバーライド(上書き)されません。
- 型安全な可変長引数リストがパラメータ配列を通してサポートされています。
8.8.4 プロパティ
プロパティはフィールドであるかのように振る舞うメンバです。
プロパティには二種類あります。スカラーとインデックスです。
スカラー・プロパティは CLI オブジェクトやクラスにフィールド・アクセスを可能とします。
スカラー・プロパティの例としては、文字列の長さや、フォントの大きさ、ウィンドウ・キャプション、そして、顧客の名前などが含まれます。
インデックス・プロパティは CLI オブジェクトに配列のようなアクセスを可能とします。
インデックス・プロパティの例としては、ビット配列クラスなどです。
プロパティはフィールドの革新的な拡張です――双方が型に関連づけられた名前付けメンバであり、スカラー値とスカラー・プロパティにアクセスする構文が同じであり、そのまま、配列とインデックス・プロパティにもアクセスできます。
しかしながら、フィールドとは異なり、プロパティは保管領域位置を示している訳ではありません。
その代わり、プロパティは、その値が読み込まれたり書き込まれたりしたとき実行される、特定の構文の
アクセス関数を持っています。
プロパティは property 定義によって定義されます。プロパティ定義の最初の部分はフィールドの定義とよく似ています。
第二の部分には get アクセサ、そして/または、set アクセサ関数が含まれます。
読み書きの双方ができるプロパティは get と set の双方のアクセサ関数を含んでいます。
以下の例では、X,Y の二つの読み書き可能なプロパティを持った point クラスを定義します。
public value class point {
int Xor;
int Yor;
public:
property int X {
int get() { return Xor; }
void set(int value) { Xor = value; }
}
property int Y {
int get() { return Yor; }
void set(int value) { Yor = value; }
}
point(int x, int y) {
move(x, y);
}
void move(int x, int y) { // 絶対移動
X = x;
Y = y;
}
void Translate(int x, int y) { // 相対移動
X += x;
Y += y;
}
...
};
get アクセサ関数はそのプロパティの値が読み込まれたとき、呼び出されます。set アクセサ関数はそのプロパティの値に書き込まれたときに呼ばれます。
プロパティの定義は比較的簡単であるが、プロパティの本当の値はそれが使われたときに見えます。
例えば、X と Y のプロパティはそれらがフィールドであるかのように読み書きできます。
上の例では、プロパティはそのクラス自身が持つデータを隠蔽するために使われています。
次のアプリケーション・コードは(直接、非直接的に)それらのプロパティを利用しています。
point p1; // (0, 0) に設定
p1.X = 10; // (10, 0) に設定
p1.Y = 5; // (10, 5) に設定
p1.move(5, 7); // (5, 7) に設定
point p2(9, 1); // (9, 1) に設定
p2.translate(-4, 12); // 左に 4、上に 12 移動して(5,13)
トリビアル・プロパティ 宣言はこのように行い、
property String^ Name;
コンパイラは自動的にアクセサ関数のデフォルト実装を提供します。
デフォルト・インデックス・プロパティはインスタンスへの直接的な配列アクセスを許しています。
[注意:他の言語ではデフォルト・インデックス・プロパティを”インデクサー”として参照します。]
例として、Stack クラスを考えます。
このクラスの設計者は Push や Pop の操作を必要とせずに、スタックのアイテムを取り出したり、変更したりできるように配列的アクセスを曝したいでしょう。
それは、Stack がリンクド・リストとして実装されていたとしても、配列アクセスの簡便性を提供します。
デフォルト・インデックス・プロパティの定義はプロパティ定義と同様です。
主たる違いは、デフォルト・インデックス・プロパティはインデックス・パラメータを含み名前を持たないということでしょう。
インデックス・パラメータは鍵括弧[]の間に与えます。この例は、
public ref class Stack {
public:
ref struct Node {
Node^ Next;
Object^ Value;
Node(Object^ value) : Next(nullptr), Value(value) {}
Node(Object^ value, Node^ next) {
Next = next;
Value = value;
}
};
private:
Node^ first;
Node^ GetNode(int index) {
node^ temp = first;
while (index > 0) {
temp = temp->Next;
index--;
}
return temp;
}
bool ValidIndex(int index) { ... }
public:
property Object^ default[int] { // デフォルト・インデックス・プロパティ
Object^ get(int index) {
if ( !ValidIndex(index) )
throw gcnew Exception("インデックスが配列の範囲外です。");
else
return GetNode(index)->Value;
}
void set(int index, Object^ value) {
if ( !ValidIndex(index) )
throw gcnew Exception("インデックスが配列の範囲外です。");
else
GetNode(index)->Value = value;
}
}
Object^ Pop() { ... }
void Push(Object^ o) { ... }
...
};
int main() {
Stack^ s = gcnew Stack;
s->Push(1);
s->Push(2);
s->Push(3);
s[0] = 33; // 先頭アイテムに 3 に変わって 33 が参照される。
s[1] = 22; // 真ん中のアイテムが 2 に変わって 22 が参照される。
s[2] = 11; // 最後のアイテムが 1 に変わって 11 が参照される。
}
Stack クラスのデフォルト・インデックス・プロパティを示しています。
[注意:より効果的な Stack の実装は generics を使って行うべきでしょう。]
8.8.5 イベント
イベントは CLI オブジェクトやクラスに通知を提供することができるメンバです。クラスは event 宣言(それは
event識別子が加わっているけれど、フィールド宣言に似ている)とイベント・アクセサ関数の随意なセットを与えられることでイベントを定義します。この宣言の型はデリゲート型(
§8.7 )へのハンドルでなければなりません。
例としては、
public delegate void EventHandler(Object^ sender, EventArgs^ e);
public ref class Button {
public:
event EventHandler^ Click;
void Reset() {
Click = nullptr;
}
};
ボタン・クラスは
Eventhandler型のClickイベントを定義します。
ボタン・クラスの中で、Clickメンバは厳密に
EventHandler型のprivateフィールドの用です。
しかしながら、ボタン・クラスの外側では、典型的にClickメンバは+=演算子と-=演算子の左辺要素としてのみ使われます。
+=演算子はイベントのハンドラを追加し、-=演算子はイベントのハンドラを削除します。例として、
public ref class Form1 {
Button^ Button1;
void Button1_Click(Object^ sender, EventArgs^ e) {
Console::WriteLine("Button1 がクリックされました!");
}
public:
Form1() {
Button1 = gcnew Button;
// Button1_Click を Button1 のクリック・イベント用にイベント・ハンドラとして追加する。
Button1->Click += gcnew EventHandler(this, &Button1_Click);
}
void Disconnect() {
Button1->Click = gcnew EventHandler(this, &Button1_Click);
}
};
はボタン1のClickイベントのためのイベント・ハンドラとして、Button1_Click を追加している クラス Form1 を示しています。
Disconnect 関数で、そのイベント・ハンドラは削除されます。
より制御を求めるプログラマはその追加、削除関数を明示的に提供することができます。
例えば、ボタン・クラスは以下のように書き直すこともできます。
public ref class Button {
EventHandler^ handler;
public:
event EventHandler^ Click {
void add( EventHandler^ e ) { handler += e; }
void remove( EventHandler^ e ) { handler -= e; }
}
...
};
この変更はクライアントのコードにはなんの変更ももたらしませんが、この変更はボタン・クラスに実装上の柔軟性をより許します。
例としては、Clickのためのイベント・ハンドラはフィールドに明示する必要がありません。
トリビアル・イベント宣言のために、このような
event Eventhandler^ Click;
コンパイラは自動的にアクセサ関数のデフォルト実装を提供します。
8.8.6 静的演算子
標準 C++ の演算子オーバーロードに加えて、C++/CLI は静的であり/ないし、^ 型のパラメータを取る演算子を定義する能力を提供します。
下記の例は、整数ベクター・クラスの一部を示しています。
public ref class IntVector {
int array<int>^ values;
public:
property int Length { // プロパティ
int get() { return values->Length; }
}
property int default[int] { // デフォルト・インデックス・プロパティ
int get(int index) { return values[index]; }
void set(int index, int value) { values[index] = value; }
}
IntVector(int length);
IntVector(int length, int value);
// 単項演算子(負数)
static IntVector^ operator-(IntVector^ iv) {
IntVector^ temp = gcnew IntVector(iv->Length);
for( int i=0; i < iv->Length; ++i ) {
temp[i] = -iv[i];
}
return temp;
}
static IntVector^ operator+(IntVector^ iv, int val) {
IntVector^ temp = gcnew IntVector(iv->Length);
for( int i=0; i < iv->Length; ++i ) {
temp[i] = iv[i] + val;
}
return temp;
}
static IntVector^ operator+(int val, IntVector^ iv) {
return iv + val;
}
...
};
int main() {
IntVector^ iv1 = gcnew IntVector(4); // 4要素で値は0
IntVector^ iv2 = gcnew IntVector(7, 2); // 7要素で値は2
iv1 = -2 + iv2 + 5;
in2 = -iv1;
}
8.8.7 インスタンス・コンストラクタ
標準C++と異なり、C++/CLI は静的コンストラクタ(
§8.8.9 )をサポートします。
そのため、この仕様では標準C++ によって定義されたコンストラクタをインスタンス・コンストラクタと言及しています。
8.8.8 デストラクタとファイナライザ
標準 C++ では、クリーンアップ・コードは伝統的にデストラクタによってカプセル化されています。
このアプローチは簡便で強力なリソース抽象化の手法を提供します。リソース・リークはデストラクタが呼ばれなかったとき、発生します。
ガベージ・コレクタを持つことで、C++/CLI はオブジェクトがもはや参照されなくなったときに代わりに実行されるクリーンアップ・コードを記述するメカニズムを提供します。
結果として、ref クラスはその型のインスタンスが保持するリソースを掃除する責任を持つ二つの特殊なメンバ関数を持つことになります。:それはデストラクタとファイナライザです。
- デストラクタ : デストラクタは決定的クリーンアップとオブジェクトの生存期間の終了を提供します。標準 C++ にあるように、デストラクタはオブジェクトの基底クラスとメンバのクリーンアップを、そのコンストラクタの過程とは逆順に行います。おのおのの ref クラス毎に、順番に、デストラクタはユーザーが記述したコードを実行し、クラスに付加されているメンバおのおののデストラクタを呼び出し、基底クラス毎のデストラクタを呼び出します。デストラクタの主たる優位性は、プログラムの決定的な場所において呼び出され、ガベージ・コレクションを待っているよりも早く、リソース解放を行うという優位性を持っている点です。
- ファイナライザ : ファイナライザは非決定的クリーンアップを提供します。ファイナライザはガベージ・コレクションの間に実行される"最後のチャンス"関数で、主にデストラクタが実行されていないガベージ・コレクション上のオブジェクトに対して実行されます。ファイナライザは値型(ネイティブ・ヒープから領域確保したネイティブ・ポインタ参照などの)を持つデータ・メンバによって表現されるリソースをデストラクタが実行されていない場合に確実に掃除するので特に便利です。ファイナライザは時々、オブジェクトにアクティブな参照がもうないと決定されたガベージ・コレクトの後に実行されます。(ファイナライザを持つことによるパフォーマンスのペナルティがあります。)
インスタンスが自身にリソースを持つ ref クラスはデストラクタを常に持つべきです。ファイナライザを持つクラスも同様に、決定的なクリーンナップと素早いリソース解放を行えるよう、常にデストラクタを持つべきでしょう。
しかし、デストラクタを持つクラスはファイナライザを持っている必要はありません。
ref struct R {
~R() { ... } // デストラクタ、ファイナライザなし
};
インスタンスが値型で表現されるリソース(ポインタのような)を持つ ref クラスはファイナライザを持つべきでしょう。
(これらは、つねにいくつかのファイナライズ可能な親クラスを持っているわけでないクラスにファイナライザを導入することでパフォーマンス的なペナルティを負うかもしれません。
そのようなよくデザインされたクラス階層はクラス階層の葉となる値型によって表現されるリソースを限定するでしょう。)
リソースを表現する値型を持たない ref クラスは依然、デストラクタを持ち得ますが、ファイナライザを持つべきではありません。
ref struct R {
~R() { ... } // デストラクタ
!R() { ... } // ファイナライザ
};
C++/CLI は任意の ref クラス T のデストラクタとファイナライザ構文を
CLI dispose パターンを使って実装し、五つの関数、(Dispose(), Dispose(bool), Finalize(), __identifier("~T")() そして、__identifier("!T")() )を利用可能にします。そして、これらの関数の定義は必要に応じてコンパイラに生成されます。これらのクリーンアップ・メカニズムは C++/CLI プログラマからは隠蔽されています。C++/CLI では、クリーンアップを実行する適切な方法は、以下のようにデストラクタとファイナライザ中に全てのクリーンアップ・コードを配置することです。
- ファイナライザは値型で表現される任意のリソースを掃除するべきです。
- デストラクタは可能な限り最大の掃除を行うべきです。これを促すために、プログラマはデストラクタからファイナライザを呼び、デストラクタにそれ以外のクリーンアップ・コードを書くべきでしょう。デストラクタはオブジェクトから参照によって ref クラスの状態に安全にアクセスすることができますが、ファイナライザはできません。
ref クラスのために、ファイナライザとデストラクタの双方はそれらが複数回実行でき、完全にコンストラクトされていないオブジェクトでも実行できるよう記述されなければなりません。
8.8.9 静的コンストラクタ
静的コンストラクタはインスタンス・メンバより、むしろ、クラスの静的メンバを初期化する必要があるときの挙動を実装した ref や値クラスの静的メンバ関数です。静的コンストラクタはパラメータを持たず、private でなければなりません。そして、それらは明示的に呼び出すことはできません。クラスの静的コンストラクタはランタイムによって自動的に呼び出されます。[注意:静的コンストラクタは最初の一回以外に呼び出されることを防ぐためにprivateである必要があります。]
例、
public ref class data {
private:
initonly static double coefficient1;
initonly static double coefficient2;
static Data() {
// coefficient値をいくつかのソースから取得する
coefficient1 = ...;
coefficient1 = ...;
}
public:
...
};
は、二つの初期化のみ静的フィールドを初期化している静的コンストラクタを持つ Data クラスを示しています。
8.8.10 継承
ref クラスを使うとき、C++/CLI は ref クラスの単一継承のみをサポートします。しかしながら、インターフェイスの多重継承は許されます。
8.8.10.1 関数オーバーライド(上書き)
標準C++では、派生クラスが基底クラスの仮想関数として同名、同一パラメータ・リスト、同一 cv-修飾の関数が与えられると、派生クラスの関数は派生クラスの関数が virtual 宣言されていなくても常に基底クラスの関数を上書きしています。
struct B {
virtual void f();
virtual void g();
};
struct D {
virtual void f(); // D::f は B::f を上書きする。
void g(); // D::g は B::g を上書きする。
};
我々はこれを
暗黙のオーバーライド(上書き)と言及します。( virtual 指定子としては D::f はオプションであり、そこに virtual が存在していることは、本来の明示的なオーバーライドを示していません。)暗黙のオーバーライドはバージョン化(
§8.13 )の手法を獲得しているため、暗黙のオーバーライドは C++/CLI コンパイラによって診断されなければなりません。
C++/CLI は標準C++で提供されていない二つの仮想関数オーバーライド特徴をサポートしています。これらの特徴は ref クラスで手に入ります。その二つは明示的オーバーライドと名前付けオーバーライドです。
明示的なオーバーライド: C++/CLI においては、以下のようにすることができます。
- 派生クラスの関数は override 関数修飾子を使うことで、基底クラスの同じ名前、パラメータ型リスト、cv-修飾を持つ仮想関数を、例え、その様な関数が基底クラスに存在しないようなプログラム的に不正な状態であっても、明示的に上書きします。
- 派生クラスの関数は new 関数修飾子を使うことによって、基底クラスが同じ名前、パラメータ型リスト、cv-修飾を持っている仮想関数を明示的に上書きしません。
struct B {
virtual void F();
virtual void G();
};
struct D : B {
virtual void F() override; // D::F は B::F を上書きする。
virtual void G() new; // D::G は B::G を上書きしない。D::G は B::G を隠す。
};
D::Fは virtual でなければならず、そのようにマークされていなければなりません。他方、D::G は virtual である必要はなく、もし、仮想関数でないのであれば、その様にマークされるべきではありません。
名前付けオーバーライド: override 修飾子を使う代わりに、オーバーライドする関数の名前付けを含む
override-specifier(上書き指定子) を使うことで、同様のものを得ることができます。
このアプローチは我々に、同じパラメータ型リストを提供することで、違う名前を持つ関数を上書きすることを許してくれます。
struct B {
virtual void F();
};
interface struct I : {
virtual void G();
};
ref struct D : B, I {
virtual void X() = B::F, I::G {} // D::X は B::F と I::G を上書きする
};
override-specifierを持つ全ての関数宣言が
virtual を使うことは強制です。
明示的な、そして、名前付けのオーバーライドは以下のように組み合わせることが可能です。
struct B {
virtual void F();
virtual void G();
};
struct D : B {
virtual void F() override = B::G {}
};
任意の与えられたクラスに対して、関数は一度だけ上書きすることができます。それ故に、暗黙的な、または、明示的なオーバーライドが名前付けオーバーライドとして同じものに実施されることは、プログラムとして不正です。
struct B {
virtual void F();
virtual void G();
};
struct D : B {
virtual void F() override = B::F; // error : B::F は二度上書きされるため
virtual void G() override; // B::G は上書きされる
virtual void H() = B::G {} // エラー:B::G は二度上書きされるため
};
[注意:もし、基底クラスがテンプレート型パラメータに依存している場合、基底クラスからの仮想関数の名前付けオーバーライドはインスタンス生成の時点まで発生しません。次のように、
template<typename T>
ref class R : T {
public:
virtual void f() = T::G { ... }
};
T::G は依存名称です。]
8.9 値クラス
値クラスはフィールドやメンバ関数を含むことができるデータ構造を表現する形式において ref クラスとよく似ています。
しかしながら、ref クラスと違って、値クラスはヒープ領域確保を必要としていません。
値クラスの変数は値クラスのデータに直接含まれており、ref クラスの変数はデータのハンドルに含まれています。
値クラスは値構文を持つ小さなデータ構造に特に便利です。複素数、座標系上の点、辞書構造のキー・値対などはすべて値クラスの良い例です。
これらのデータ構造のキーは、それらはほとんどフィールドを持たず、継承や参照識別を必要としなかったり、参照の代わりに値のコピーを代入するような値的な意味論をもって簡単に実装されます。
単純型--int や、double、boolと言ったもの--は実際には全て値クラス型です。演算子オーバーロードと値クラスを使って、新たに作成される「基本」型を実装することが可能です。
value struct Point {
int x, y;
Point(int x, int y) {
this->x = x;
this->y = y;
}
};
8.10 インターフェイス
インターフェイスは契約を定義します。インターフェイスを実装したクラスは、インターフェイスで宣言された全ての関数、プロパティ、そして、イベントの実装によって契約を固持しなければなりません。
例としては、
delegate void Eventhandler(Objcet^ sender, EventArgs^ e);
interface class IExample {
void F(int value);
property bool P { bool get(); }
property double default[int] {
double get(int);
void set(int, double);
}
event EventHandler^ E;
};
これは関数 F、読み込みスカラー・プロパティ P、デフォルト・インデックス・プロパティ、そして、イベント E を含むインターフェイスを示しており、その全てが暗黙的に public です。
インターフェイスは継承構文を使って実装されます。
interface class I1 { void F(); }; // F は暗黙のうちに virtual abstract である。
ref class R1 : I1 { virtual void F() { /* I1::F を実装 */ }
インターフェイスは一つ以上の他のインターフェイスの実装を要求できます。例えば、
interface class IControl {
void Paint();
};
interface class ITextBox : IControl {
void SetText(String^ text);
};
interface class IListBox : IControl {
void SetItems(array<String^>^ items);
};
interface class IComboBox : ITextBox, IListBox {};
IComboBox を実装するクラスは ITextBox、IListBox、そして、IControl もまた実装しなければなりません。
クラスは複数のインターフェイスを実装することができます。例中の
interface class IDataBound {
void Bind(Binder^ b);
};
public ref class EditBox : Control, IControl, IDataBound {
public:
void Paint() {...}
void Bind(Binder^ b) {...}
};
EditBox クラスは ref クラス Control から派生し、IControl と IDataBound の双方を実装します。
上の例では、インターフェイス関数は暗黙のうちに実装されています。
C++/CLI はこれらのメンバが public になってしまうことを避けるクラスの実装を許す、これらの関数のもう一つの違う実装方法を提供します。
インターフェイス関数は
§8.8.10.1 に示した名前付けオーバーライド(上書き)構文を使って、明示的に実装をすることができます。
例えば、EditBox クラスでは暗黙の上書きの代わりに IControl::Paint と IDataBound::Bind 関数を提供して実装されています。
public ref class EditBox : IControl, IDataBound {
private:
virtual void Paint() = IControl::Paint {...}
virtual void Bind(Binder^ b) = IDataBound::Bind {...}
};
インターフェイス・メンバは、各々のメンバを明示的に実装しているインターフェイス・メンバを指し示して、この
明示的インターフェイス・メンバと呼ばれる方法で実装しました。
int main() {
EditBox^ editbox = gcnew EditBox;
editbox->Paint(); // error : Paint は private です。
IControl^ control = editbox;
control->Paint(); // EditBox の Paint 実装を呼び出す。
}
8.11 列挙型
標準C++ はすでに列挙型をサポートしています。しかし、C++/CLI ではこの手法にいくつか興味深い拡張を提供します。例えば、
- enum はその親のアセンプリの外側に対する認識性を制御できるようにするために、 public もしくは、private に宣言することができます。
- enum の背景となる型を指定することができます。
- enum 型と/もしくは、その列挙子は属性を持つことができます。
- enum 型を強く型付けして、そのため、整数推進を持たないようにしたりするような定義をする新しい構文があります。
8.12 名前空間とアセンブリ
プログラムは System::Console のような、わずかなシステムが提供するクラスへの依存を別にして、今までのそれ自身で自立しているよう提示されていました。
しかしながら、実世界でのアプリケーションでは、それぞれ別々にコンパイルされた、いくつもの異なる欠片によって成り立っていると言うことが、遙かに一般的です。
例えば、企業のアプリケーションは、あるものは内部的に開発され、あるものは独立系ソフトウェア・ベンダーから購入した、いくつかの異なるコンポーネントに依存しているでしょう。
名前空間と
アセンブリはこのコンポーネント・ベース・システムを可能にします。
名前空間は論理組織化システムを提供します。
名前空間はプログラムの
内部的な組織化システムとしても、
対外的な組織化システムとしても――他のプログラムにさらされているプログラム要素を提示する方法で――双方で使われます。
アセンブリは物理的なパッケージ化と展開に使われます。アセンブリは、型や、それらの型を実装するのに使っている実行コードや、他のアセンブリへ参照を含むことができます。
名前空間とアセンブリの使い方をデモンストレートするために、この節では初めに示した「Hello, world」プログラムに再登場願い、それを二つの断片に分割します。
歓迎を表示する関数を含んだクラス・ライブラリとその関数を呼ぶコンソール・アプリケーションに。
クラス・ライブラリは DisplayMessage と名付けたたった一つのクラスを含みます。例えば、
// DisplayHelloLibrary.cpp
namespace MyLibrary {
public ref struct DisplayMessage {
static void Display() {
Console::WriteLine("hello, world");
}
};
}
次のステップでは、DisplayMessage クラスを使うコンソール・アプリケーションを書きます。
例としては、
// HelloApp.cpp
#using <DisplayHelloLibrary.dll>
int main() {
MyLibrary::DisplayMessage::Display();
}
CLI ライブラリのクラスや関数を使うときには、ヘッダをインクルードする必要はありません。
代わりに、上に示したように、 <...> にアセンブリ名を閉じこめた #using 指定子を通じてライブラリ・アセンブリを参照しれます。
記述されたコードは DisplayMessage を含むクラス・ライブラリと、 main 関数を含むアプリケーションとにコンパイルされます。
このコンパイル作業の過程の詳細は、使ったツールやコンパイラによって異なるでしょう。
コマンドライン・コンパイラは次のようなコマンドライン呼び出しによってクラス・ライブラリとアプリケーションのコンパイルを可能とします。
cl /LD DisplayHelloLibrary.cpp
cl HelloApp.cpp
これらは DisplayHelloLibrary.dll と名付けられたクラス・ライブラリと HelloApp.exe と言う名前のアプリケーションを生成します。
8.13 バージョン化
バージョン化とは互換性の振る舞いを持ったまま、時を越えてコンポーネントを発展させていくプロセスです。
もし再コンパイルすることで以前のバージョンに依存したコードが新しいバージョンと一緒に動作できるのなら、コンポーネントの新しいバージョンは
ソース互換です。
対照的に、もし古いバージョンに依存したアプリケーションが再コンパイルすることなく新しいバージョンでも動くのなら、コンポーネントの新しいバージョンは
バイナリ互換です。
基底クラスの作者が Base と名付けた基底クラスの状況について考えてみましょう。
最初のバージョンでは、Base クラスは F という関数を含んでいません。Base から Derived という名前のコンポーネントを派生し、関数 F を導入します。
この Derived クラスは、それが依存する Base クラスと一緒に、多数のクライアントとサーバーを展開する顧客の元にリリースされます。
public ref struct Base { // version 1
...
};
public ref struct Derived : Base {
virtual void F() {
Console::WriteLine("Derived::F");
}
};
今まではこれでうまくいっているのですが、バージョン化トラブルがここから始まります。Base の作者が新しいバージョンを出し、それ自身に関数 F を与えました。
public ref struct Base { // version 2
virtual void F() { // version 2 で追加
Console::WriteLine("Base::F");
}
};
この Base の新しいバージョンは初期バージョンとソース、バイナリの両互換性を維持するべきです(単に関数を追加することもできないのなら、基底クラスは決して発展することはできないでしょう)。
不幸なことに、Base クラスの新しい F は Derived クラスの F の意味を不明確にします。
Derived は Base の F を上書きしたのでしょうか? これは見込みがなさそうです。
Derived がコンパイルされたとき、Base は F を持っていなかったのですから!
さらに、仮に Derived の F が Base の F を上書きしたとして、その時、Base によって指定された契約―― Derived が記述されたときにはまだ指定されていなかった契約を、堅持しなければならないのです。
多くの場合では、これは不可能です。例えば、Base の F は常に基底クラスの F を呼び出すように要求するかもしれないからです。
Derived の F はそのような契約を堅持できません。
C++/CLI では開発者がその意図を明確に定めることで、このバージョン化問題に取り組みます。
オリジナルのコード例では、Base クラスが関数 F を持つまでは、コードは明快です。
F という基底クラスの関数が存在しないので、明らかに、Derived の F は基底クラスの関数を上書きするものではなく、新規の関数を意図しています。
もし、Base クラスが F を追加し、新しいバージョンを輩出したとしても、Derived クラスのバイナリ・バージョンの意図は未だに明快です。――Derived クラスの F は意味的に無関係なので、オーバーライドとして取り扱われるべきではありません。
しかしながら、Derived が再コンパイルされると、その意味が不明確になります。――Derived の作者はその F を Base の F を上書きするつもりとも、それを隠すとも知れないのです。
デフォルトでは、コンパイラは Derived の F で Base の F を上書きします。ですが、この行動の振る舞いは Derived が再コンパイルされなかった場合の時の意味を繰り返していません。
もし、Derived クラスの F が Base クラスの F と意味的に無関係なら、Derived クラスの作者はこの意図を F の宣言に new 関数修飾子を使うことで表明できます。
public ref struct Base { // version 2
virtual void F() { // version 2 で追加
Console::WriteLine("Base::F");
}
};
public ref struct Derived : Base { // version 2a: new
virtual void F() new {
Console::WriteLine("Derived::F");
}
};
または、Derived クラスの作者はさらなる調査を行い、Derived の F は Base の F を上書きするべきだと決断するかもしれません。
この意図は、下に示すとおり、override 関数修飾子を使うことで明示的に指定することができます。
public ref struct Base { // version 2
virtual void F() { // version2 で追加
Console::WriteLine("Base::F");
}
};
public ref struct Derived : Base { // version 2b: override
virtual void F() override {
Base::F();
Console::WriteLine("Derived::F");
}
};
Derived クラスの作者はもう一つのオプション、F の名前を変更することで完全に名前衝突を回避する、を持っています。
この変更は Derived クラスのソースとバイナリの互換性を破壊するかもしれませんが、シナリオによって互換性の重要度は変わります。
もし、Derived クラスが他のプログラムにさらされていなければ、F の名前を変更することは、プログラムの可読性――そこにはもう F の意味についてなんの混乱ももはや生じないでしょうから――を証明し、良い考えと言えます。
8.14 属性
標準 C++ は一定の宣言要素を持っています。例えば、クラス中の関数のアクセス可能性は、それを public, protected, private で宣言することで指定することができます。
C++/CLI ではこの許容性を、プログラマが新たな種類の宣言情報を考案し、様々なプログラム・エンティティにこの宣言情報を付与し、実行時にこの宣言情報を取得できるよう、一般化します。
プログラムは、
attributes(属性)を定義し、使用することで、この追加の宣言情報を指定します。
例えば、フレームワークは、開発者がプログラム要素からそのドキュメントへの対応を提供できるよう、クラスや関数などのプログラム要素に HelpAttribute を定義することができます。例、
[AttributeUsage(AttributeTargets::All)]
public ref class HelpAttribute : Attribute {
String^ url;
public:
HelpAttribute(String^ url) {
this->url = url;
}
String^ Topic;
property String^ Url {
String^ get() { return url; }
}
};
は、HelpAttribute という名前の、一つの配置パラメータ(String^ url)を持ち、一つの名前付けパラメータ(String^ Topic)を持つ、属性クラスを定義しています。
配置パラメータは属性クラスの public なインスタンス・コンストラクタに規定のパラメータとして宣言され、名前付けパラメータは public な非静的読み書き可能なフィールドと属性クラスのプロパティです。
便利さのために、属性が与えられたとき、属性名の使用は名前から Attribute 語尾を取り除いてもかまいません。
例として、
[Help("http://www.mycompany.com/.../Class1.htm")]
public ref class Class1 {
public:
[Help("http://www.mycompany.com/.../Class1.htm", Topic = "F")]
void F() {}
};
これははいくつかの Help 属性の使い方を示しています。
プログラム要素に与えられた属性情報は、リフレクションのサポートを使って実行時に取得することができます。例えば、
int main() {
Type^ type = Class1::typeid;
array<Object^>^ arr = type->GetCustomAttribute(HelpAttribute::typeid, true);
if ( arr->Length == 0 )
Console::WriteLine("Class1 は Help 属性を保持していません。");
else {
HelpAttribute^ ha = (HelpAttribute^) arr[0];
Console::WriteLine("Url = {0}, Topic = {1}", ha->Url, ha->Topic);
}
}
これは Class1 が Help 属性を持っているかどうかをチェックし、その属性が存在したら、関連した Topic と Url の値を書き出しています。
8.15 ジェネリクス
ジェネリック型、ジェネリック関数とは――
ジェネリクス(総称的、汎化)と総称される――型のパラメーター化を許すために CLI によって定義されたフィーチャーのセットです。
ジェネリクスは仮想実行システム (VES) によって実行時にインスタンス化されるという点で、コンパイル時にコンパイラによって展開されるテンプレートと異なっています。
ジェネリック定義は refクラス、値クラス、インターフェイス・クラス、デリゲート、または、関数でなければなりません。
8.15.1 ジェネリクスの作成と消費
以下に、私たちは template の代わりに generic キーワードが使われるという点を除いてテンプレートと同じ記述方法で、
型パラメータ ItemType を指定する Stack ジェネリック・クラス定義を作成します。
この型パラメータは使用時に実際の型が指定されるまでの場所取りのようなものとして機能します。
generic<typename ItemType>
public ref class Stack {
array<ItemType>^ items;
public:
Stack(int size) {
items = gcnew array<ItemType>(size);
}
void Push(Itemtype data) {...}
ItemType Pop() {...}
};
ジェネリック・クラス定義 Stack を使うとき、私たちはジェネリック・クラスに使われる実際の型を指定します。
この場合、名前の後ろに角括弧<>を使って
type argument (型引数)として int 型を指定した Stack クラスを説明します。
Stack<int>^ s = gcnew Stack<int>(5);
そうすることで、私たちは新しい
生成型、その Stack 定義の中にある全ての ItemType を与えられた型引数 int が置き換えた Stack<int> を作成しました。
もし、私たちが Stack に int 以外のアイテムを蓄えたいと思ったとき、Stack から 新しい型引数を指定して、違った生成型を作成しなければなりません。
私たちは単純な Customer 型を持っていると仮定し、それを蓄えるために Stack を使いたいと望みました。そのために、私たちは単純に Stack の型引数として Customer クラスを使います。
コードは簡単に再利用できて、
Stack<Customer^>^ s = gcnew Stack<Customer^> (10);
s->Push(gcnew Customer);
Customer^ c = s->Pop();
もちろん、いったん、型引数に Customer 型を与えて Stack 型を作成したら、私たちは Customer オブジェクト(もしくは Customer から派生したクラスのオブジェクト)を蓄えることのみに今や強く限定されます。テンプレートのように、ジェネリクスは強く型付けされています。
ジェネリック型定義は任意の数の型パラメータを持つことができます。キーに対して値が蓄えられる単純な Dictionary ジェネリック・クラス定義を作ると仮定しましょう。
私たちは次のような、二つの型パラメータを宣言しているジェネリック版を定義できます。
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {...}
property ElementType default[KeyType] { // インデックス・プロパティ
ElementType get(KeyType key) {...}
void set(KeyType value, ElementType key) {...}
}
};
Dictionary 型を使うときには、角括弧内に二つの型引数を与える必要があります。
そして、Add 関数を呼んだり、インデックス・プロパティを使ったりするとき、コンパイラは私たちが正しい型を与えていることを検証します。
Dictionary<String^, Customer^>^ dict = gcnew Dictionary<String^, Customer^>;
dict->Add("Peter", gcnew Customer);
Customer^ c = dict["Peter"];
8.15.2 拘束
多くの場合、私たちは与えられた型パラメータのデータを格納する以上のことをすることを欲しています。
しばしば、私たちはまた、私たちのジェネリック型定義の中で命令を実行するのに型パラメータのメンバを使いたくなります。
例えば、私たちの Dictionary 型の Add 関数中で、以下のように、与えられたキーの CompareTo 関数を使ってアイテムを比較したくなります。
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
...
if ( key->CompareTo(val) < 0 ) { ... } // コンパイル時エラー
...
}
};
不幸なことに、KeyType 型パラメータはコンパイル時に、予期したとおり、汎化されます。
書かれているとおり、コンパイラは KeyType 型の変数 key に取得できる操作として、System::Object 型に与えられる、ToString 関数などの呼び出しのような、操作のみを想定します。
結果として、コンパイラは CompareTo 関数が見つからないので、診断メッセージを出します。
しかしながら、私たちは key 変数を プログラムがコンパイル可能になるよう CompareTo 関数を含む、IComparable インターフェイスなどに、型をキャストできます。
generic<typename KeyType, typename ElementType>
public ref class Dictionary {
public:
void Add(KeyType key, ElementType val) {
...
if ( static_cast<IComparable^>(key)->CompareTo(val) < 0 ) { ... }
...
}
};
しかし、もし、今、Dictionary 型からある型を生成し、IComparable を実装していないような型引数をキーとし