19.6 イベント
event (イベント) はクラス・オブジェクトに通知を提供することができるメンバです。
クライアントはイベントにデリゲートを追加することができるので、オブジェクトやクラスはそのデリゲートを呼び出すでしょう。
イベントは
event-definition(イベント定義) を使って宣言されます。
event-definition:
attributesopt event-modifiersopt event event-type identifier
{ accessor-specification }
attributesopt event-modifiersopt event event-type identifier ;
event-modifiers:
event-modifiersopt event-modifier
event-modifier:
static
virtual
event-type:
::opt nested-name-specifieropt type-name ^opt
::opt nested-name-specifieropt template template-id ^
event-definition (イベント定義) は
attribute (属性)(
§29 )と
event-modifier (イベント修飾子)(
§19.6.1 ,
§19.6.3 )のセットを含むことが可能です。
イベント定義の
event-type(イベント型) はデリゲート型であるべきで、それは少なくともイベントそれ自身と同じアクセス可能性であるべきです。デリゲートへのハンドルは
event type(イベント型)として知られています。
identifier (識別子) はイベントの名前を指し示します。
event-definition (イベント定義) が
property-modifier (プロパティ修飾子) static、ないし、virtual を含む時、これらの修飾子は実際にイベントのアクセサ関数全てに適用されます。
これらのアクセサ関数に同様に同じ修飾子を記述することは許されていますが、冗長です。
accessor-specification (アクセサ指定子) はイベントのアクセサ関数(
§19.6.2 )を宣言します。アクセサ関数はそのイベントを実行するのと同様に、イベントにハンドラを追加する、イベントからハンドラを削除することに関連した実行可能な命令文を指定します。
[注意:
event-type の最初の生成物の ^ は typedef した名前である
type-name を許すために随意となっています。]
(
accessor-specification が括弧区切りなのに対して)セミコロンで終わる
event-definition (イベント定義) は
trivial event (トリビアル・イベント)(
§19.6.4 )を定義します。
トリビアル・イベントのための三つのアクセサ関数はプライベート背景ストレージと共にコンパイラによって自動的に提供されます。
accessor-specification を括弧区切りで終わらせる
event-definition (イベント定義) は
non-trivial event (非トリビアル・イベント) です。
[例:次の例はどうやってイベント・ハンドラが Button クラスのインスタンスにアタッチするかを示しています。
public delegate void EventHandler(Object^ sender, EventArgs^ e);
public ref struct MyButton : Control {
event EventHandler^ Click;
...
};
public ref class LoginDialog : Form
{
MyButton^ OkButton;
MyButton^ CancelButton;
public:
LoginDialog() {
OkButton = gcnew MyButton();
OkButton->Click += gcnew EventHandler(this, &LoginDialog::OkButtonClick);
CancelButton = gcnew MyButton();
CancelButton->Click += gcnew EventHandler(this, &LoginDialog::CancelButtonClick);
}
void OkButtonClick(Object^ sender, EventArgs^ e) {
// Handle OkButton->Click event
}
void CancelButtonClick(Object^ sender, EventArgs^ e) {
// Handle CancelButton->Click event
}
};
ここで、LoginDialog コンストラクタは二つの MyButton インスタンスを生成し、Click イベントにイベント・ハンドラをアタッチしています。]
イベント・アクセサ関数は適切な型を持ったデリゲートに拘束づけられることができます。
もし、add と remove アクセサ関数がデリゲートのストレージにアクセスすると、スレッド安全であるために、それらは互いにインスタンス・イベントや静的イベントのためにオブジェクトを含む排他ロックを行うべきです。その様なロックは属性 MethodImpl (MethodImplOptions::Syncronized)を add と remove アクセサ関数に適用することによって獲得されることができます。
メタデータの詳細は、
§34.7.6 を参照のこと。
19.6.1 static とインスタンス・イベント
イベント宣言が static 修飾子を含む時、そのイベントは
static event (静的イベント) であると言われます。static 修飾子がない時には、そのイベントは
instance event (インスタンス・イベント) であると言われます。
19.6.2 アクセサ関数
イベントの
accessor-specification (アクセサ指定子) はイベントの実行と同様にイベントへのハンドラの追加とイベントからのハンドラの削除に関連した実行可能な命令文を指定します。
イベントの
accessor-specification (アクセサ指定子) は次の三つのアクセサ関数以外に含むべきではありません。
- ひとつは add と呼ばれる関数で、それは add accessor function として参照され、
- ひとつは raise と呼ばれる関数で、それは raise accessor function として参照され、
- ひとつは remove と呼ばれる関数で、それは remove accessor function として参照されます。
非トリビアル・イベントは add アクセサ関数と remove アクセサ関数の双方を含むべきです。もし、そのイベントが raise アクセサ関数を持っていなければ、それはコンパイラによって自動的に提供されません。
もし、add アクセサ関数か remove アクセサ関数のどちらか一つ、ないし、両方とも持っていないイベントが含まれていた場合、そのプログラムは不正です。
add アクセサ関数と remove アクセサ関数はおのおの、
event-type 型の一つのパラメータを取るべきであり、それらの返却型は void であるべきです。
raise アクセサ関数のパラメータ・リストは
event-type 型のデリゲートのパラメータ・リストと厳密に対応しているべきであり、その返却型は
event-type デリゲートの返却型であるべきです。
[注意:トリビアル・イベントの方を一般的には使う方がいいでしょう。なぜなら、非トリビアル形式の利用はスレッド安全性を考慮する必要があるからです。]
イベントが呼び出された時、raise アクセサ関数が呼び出されます。
[例:
using namespace System::Runtime::CompilerServices;
public delegate void EventHandler(Object^ sender, EventArgs^ e);
public ref class Button : Control {
EventHandler^ action;
public:
event EventHandler^ Click {
[MethodImpl(MethodImplOptions::Synchronized)]
void add(EventHandler^ d) { … }
[MethodImpl(MethodImplOptions::Synchronized)]
void remove(EventHandler^ d) { … }
void raise(Object^ sender, EventArgs^ e) { … }
}
};
]
19.6.3 virtual、sealed、abstract、そして、アクセサ関数の上書き(オーバーライド)
abstract 修飾子を持つアクセサ関数は abstract であり、virtual です。なんの実装も提供されません。代わりに、非 abstract 派生クラスはイベントを上書きすることでアクセサ関数のために自身の実装を提供することを要請されます。abstract なアクセサ関数はまた virtual 宣言されるべきです。
abstract と override 修飾子の双方を含むイベント・アクセサ関数はアクセサ関数を abstract であり、基底イベント・アクセサ関数を上書きするものであると指定します。
継承した仮想イベントのアクセサ関数は同名のイベント宣言を含むことで派生クラスで上書きされることができます。これは
overriding event declaration (上書きイベント宣言) として知られています。上書きイベント宣言は新しいイベントを宣言しません。代わりに、それは単に存在する仮想イベントのアクセサ関数の実装を特殊化します。
アクセサ関数に sealed を宣言することで派生クラスがアクセサ関数を上書きすることを妨げます。
virtual, sealed, override, abstract アクセサ関数の構文は、厳密に virtual, sealed, override, abstract 関数と同様です。
19.6.4 トリビアル・イベント
トリビアル・イベントはセミコロンで終わる
event-definition (イベント定義) によって定義されます。(
accessor-specification (アクセサ指定子) が括弧区切りであるのに対して)[例:
ref struct S {
event SomeDelegateType^ E;
};
]
もし、イベント・ハンドラが何も追加されていなければ、フィールドは nullptr を含むます。トリビアル・イベントのために領域確保された任意のプライベート背景ストレージの名前は実装によって予約済みにされているべきです。
イベント・ハンドラを持たないトリビアル・イベントが起動されると、イベント・デリゲート型の返却値のデフォルト値を返します。例外を投げません。
19.6.5 イベント呼び出し
プログラマが提供するかコンパイラが生成する raise 関数を持つイベントは、関数呼び出し構文を使って呼び出されることができます。
明確に言えば、イベント E は E(
delegate-argument-list) を使って呼び出されることができ、それは引数リストとして
delegate-argument-list で呼び出された raise アクセサ関数のものを結果とします。
raise 関数の明示的な呼び出しも許されています。
raise アクセサ関数なしのイベントは関数呼び出し構文を使って呼び出されることはできません。
その代わり、デリゲートの Invoke 関数で直接呼び出されるべきでしょう。
19.7 静的(static)演算子
ref クラスの演算子の定義をサポートするために、C++/CLI は静的演算子関数を許します。
演算子のためのルールはおおまかに標準 C++ のまま変わらず残ります。しかしながら、標準C++(§13.5/6 )の次のルールは静的メンバ関数を許すために拡張されます。
「静的メンバや非メンバ演算子関数は非静的メンバ関数か非メンバ関数であり、あるネイティブ・クラス、ネイティブ・クラスへの参照、CLI クラス、CLI クラスへの参照、CLI クラスへのハンドル、列挙、列挙への参照、ないし、列挙へのハンドルを型とする、少なくとも一つのパラメータを持つべきです。」
非メンバ演算子関数の要請は静的演算子関数に適用されます。
標準 C++(§13.5.1/1 )の次のルールは静的メンバ関数を許すために緩和されます。
「前置単項演算子はパラメータを持たない非静的メンバ関数によって実装されるか、一つのパラメータを取る非メンバ、ないし、静的関数で実装されるべきです。」
標準 C++(§13.5.2/1)の次のルールは静的メンバ関数を許すために緩和されます。
「二項演算子は一つのパラメータを持つ非静的メンバ関数か、二つのパラメータを持つ非メンバ関数、ないし、二つのパラメータを持つ静的メンバ関数によって実装されるべきです。」
しかしながら、標準 C++ で勧告された演算子はインスタンス関数であるので、依然インスタンス関数は存在し続けるべきです。[注意:標準 C++ はこれらの演算子であることを規定します:代入演算子(
§13.5.3 )、operator()(
§13.5.4 )、operator[](
§13.5.5 )、operator->(
§13.5.6 )]
[例:
public ref class IntVector {
…
public:
static IntVector^ operator+(IntVector^ iv, int i);
static IntVector^ operator+(int i, IntVector^ iv);
static IntVector^ operator+(IntVector^ iv1, IntVector^ iv2);
static IntVector^ operator-(IntVector^ iv);
static IntVector^ operator++(IntVector^ iv);
…
};
]
クラス T 中の静的単項演算子は、型 T, T^, T%, T&, T^%, T^& の、一つのパラメータを取るべきです。クラス T 中の静的二項演算子は少なくとも型 T, T^, T%, T&, T^%, T^& のどれか一つの型を持つ、二つのパラメータを取るべきです。どちらの場合にせよ、もし、T がジェネリック・クラスであれば、上のルールを満たすパラメータは厳密に閉じられたクラスと同じ型を持つべきです。[例:
generic <typename T1, typename U1>
ref struct GR {
static bool operator!(GR^); // OK
static bool operator!(GR<T1,T1>^); // エラー
static bool operator!(GR<int,int>^); // エラー
generic <class T2, class U2>
static bool operator!(GR<T2,U2>^); // エラー
generic <class T2, class U2>
static bool operator!(GR<U2,T2>^); // エラー
generic <class T2, class U2>
static bool operator!(GR<T2,T2>^); // エラー
};
]
メタデータの詳細は、
§34.7.7 を参照のこと。
19.7.1 重複セットの候補を均一化する
標準C++ (§13.3.1/2 )は多重定義解決のためにどうやって全てのメンバ関数が
implicit object parameter (暗黙のオブジェクト・パラメータ) を持っていると見なすかを記述しています。C++/CLI はこの考えを全てのメンバ演算子関数に二つのシグネイチャを作ることによって拡張しています。その二つのシグネイチャの違いは暗黙のオブジェクト・パラメータの型です。型 T のために、最初のシグネイチャの暗黙のオブジェクト・パラメータの型は T% です。そして、二番目のシグネイチャの型は T^ です。これらのシグネイチャは多重定義解決のためだけに存在し、双方のシグネイチャともこれらのシグネイチャが作られた一つのメンバ演算子関数を指し示しています。
[根拠:これは演算子関数にロウ型(
§12.3.1 )を持つ変数とロウ型へのハンドルである変数を使った呼び出しがされることを許しています。(これは候補セットがメンバ関数と演算子関数が名前空間のスコープで演算子多重定義を比較するために必要です。)]
[例:
ref class R {
int X, Y;
public:
R(int x, int y) : X(x), Y(y) {}
R^ operator+(R^ param) {
return gcnew R(this->X + param->X, this->Y + param->Y);
}
virtual String^ ToString() override {
return String::Format("({0},{1})", X, Y);
}
};
int main() {
R^ hr = gcnew R(2, 2); // handle to raw type R
R r(10, 10); // raw type R
Console::WriteLine(hr + hr);
Console::WriteLine(r + hr);
}
]
19.7.2 ハンドラ型の演算子
ポインタと違い、いくつかのユーザー定義演算子はハンドルに適用されることが可能です。
例えば、ハンドルへの整数値の追加は(ポインタ演算でするような)ハンドルのオフセットを試みたりはしません。
むしろ、ユーザー定義演算子を検索し実行されます。標準C++ 演算子検索ルールは次のような方法に変更されます。
標準C++(§13.5.1/1 )は次のように変更されます。
「ゆえに、型 T の任意の前置単項演算子 @ において、@x はもし、x がハンドルだったら、x->operator@()、もし、x がハンドルでなければ、x.operator@()、T::operator@(x)、ないし、operator@(x) のいづれかとして解釈されます。」
標準C++(§13.5.2/1 )は次のように変更されます。
「ゆえに、型 T の任意の二項演算子 @ において、x@y はもし、x がハンドルだったら、x->operator@(y)、もし、x がハンドルでなければ、x.operator@(y)、T::operator@(x, y)、ないし、operator@(x, y) のいづれかとして解釈されます。」
[注意:C++/CLI のハンドルの等価演算子は、それらがコンパイラ生成、ないし、ユーザー定義演算子であるかのように振る舞います。]
標準C++(§13.5.3/1 )のルールは適用し続けます--代入演算子はインスタンス関数であるべきです。ハンドルの代入は決してユーザー定義代入演算子を呼び出しません。
標準C++(§13.5.4/1 )において、関数呼び出し演算子はインスタンス関数としてのみ許され続けるのですが、そのテキストは次のように変更されます。
「ゆえに、呼び出し x(arg1, ...) は、型 T のクラスオブジェクト x に、もし、T::operator(T1, T2, T3) が存在し、もし、その演算子が多重定義解決メカニズムによってもっとも適した関数として選択された場合、もし、x がハンドルであれば、x->operator()(arg1, ...)、そうでなく、もし、x がハンドルでなければ、 x.operator()(arg1, ...) として解釈されます。」
標準C++(§13.5.5/1 )において、配列要素演算子はインスタンス関数としてのみ許される続けるのですが、テキストは次のように変更されます。
「ゆえに、配列要素演算 x[y] は、型 T のクラスオブジェクト x に T::operator[](T1) が存在し、その演算子が多重定義解決メカニズムによってもっとも適した関数として選択された場合、もし、x がハンドルであれば、x->operator[](y)、そうでなく、もし、x がハンドルでなければ、 x.operator[](y) として解釈されます。」
標準C++(§13.5.6 )において、メンバ・アクセス関数はハンドルに許されます。テキストは次のように変更されます。
「演算 x->m は、型 T のクラスオブジェクト x に T::operator->() が存在し、その演算子が多重定義解決メカニズムによってもっとも適した関数として選択された場合、もし、xがハンドルであれば (x->operator->())->m と、そうでなく、x がハンドルでなければ、(x.operator->())->m として解釈されます。」
[注意:ポインタ同様、もし、メンバ・アクセス関数が存在しなければ、x->y は(*x).y として定義されます。]
[根拠:メンバ・アクセス関数は単項参照外し演算子で類似性を提供するためにハンドルがサポートされています。もし、クラスが双方の演算子に定義されているのであれば、そのクラスのメンバにアクセスする方法はありません。結果として、クラス・メンバ・アクセス演算子はハンドルを通してクラスへのメンバ・アクセスが明示的に許されているか許されていないかに静的メンバ関数であることが許されています。]
上で記述された非静的メンバ関数に加えて、CLI クラス型では operator-> が一つのパラメータを取る静的メンバ関数となり得ます。クラス R の静的 operator-> に、パラメータは R, R^, R%、ないし、より cv-qualified な別のものであるべきです。
上で提供した演算 x->m の再記述に加えて、型 T のクラス・オブジェクト x について、もし、T に静的 operator-> が存在し、その演算子が多重定義解決メカニズムによってもっとも適した関数として選択された場合、x->m は T::operator->(x)->m として解釈されます。
[注意:標準C++(§13.5.7 )で記述されているインクリメント・デクリメント演算子は CLS インクリメント・デクリメント演算子とは重大な違いを持っています。(詳細は
§19.7.3 を参照のこと)]
19.7.3 インクリメント、デクリメント演算子
C++/CLI において、静的演算子 operator++ と operator -- は前置、後置双方の演算子として振る舞います。これらの静的演算子の双方とも標準C++(§13.5.7 )に記述されている、現れない int パラメータと一緒に宣言されるべきではありません。
演算 x++ と x-- のために、後置演算子が非静的であると、次のような処理が発生します。
- もし、x がプロパティ、ないし、インデックス・アクセスに分類される場合、
- 演算 x は評価され、その結果が続く get と set アクセサ関数呼び出しで使われます。
- x の get アクセサ関数は呼び出され、返値が保存されます。
- 選択された演算子が、その引数とリテラル 0 と同じように後置演算子多重定義で選択されている引数として x の保存された値を伴って呼び出されます。
- x の set アクセサ関数がそのただ一つの、ないし、最後の引数によって返される値を伴って呼び出されます。
- x の保存された値が演算の結果となります。
- そうでなければ、
- 演算子は標準C++で規定されている処理を進めます。
演算 ++x と --x について、前置演算子が非静的であると、次のような処理が発生します。
- もし、x がプロパティ、ないし、インデックス・アクセスに分類される場合、
- 演算 x は評価され、その結果が続く get と set アクセサ関数呼び出しで使われます。
- x の get アクセサ関数が呼び出されます。
- x の get アクセサ関数の結果を引数として選択された演算子が呼び出され、その返値が保存されます。
- x の set アクセサ関数がその唯一、ないし、最後の引数として演算子呼び出しから保存された値をもって呼び出されます。
- 演算子呼び出しの保存された値が演算の結果となります。
- そうでなければ、
- 演算子は標準 C++ で規定されている処理を進めます。
演算 x++ と x-- のために、後置演算子が static であると、次のような処理が発生します。
-
もし、x がプロパティとして、ないし、インデックス・アクセスとして分類される場合、その演算子が静的演算子関数になんの現れない 0 引数を渡さない例外を持って、非静的後置演算子であるかのように、同じ振る舞いで演算が評価されます。
- そうでなければ、
- x は評価されます。
- x の値が保存されます。
- x の値を唯一の引数として、選択された演算子が呼び出されます。
- 演算子によって返された値が x の演算によって与えられた場所に代入されます。
- x の保存された値が演算の結果となります。
演算 ++x と --x について、前置演算子が static であると、次のような処理が発生します。
-
もし、x がプロパティとして、ないし、インデックス・アクセスとして分類される場合、演算は非静的前置演算子であるかのように同じ振る舞いで評価されます。
- そうでなければ、
- x は評価されます。
- x の値を唯一の引数として、選択された演算子が呼び出されます。
- 演算子によって返された値が x の評価によって与えられた場所に代入されます。
- x が演算の結果になります。
[例:次の例は実装と続けて整数ベクタ・クラスの operator++ の使い方を示します。
public ref class IntVector {
public:
…
IntVector(int vectorLength, int initValue) { … }
property int Length { … }
property int default[int] { … }
static IntVector^ operator++(IntVector^ iv) {
IntVector^ temp = gcnew IntVector(iv->Length, 0);
for (int i = 0; i < iv->Length; ++i) {
temp[i] = iv[i] + 1;
}
return temp;
}
};
int main() {
IntVector^ iv1 = gcnew IntVector(3,7);
IntVector^ iv2;
Console::WriteLine("iv1: {0}", iv1);
iv2 = iv1++;
// 次と等価:
// IntVector^ __temp = iv1;
// iv1 = IntVector::operator++(iv1);
// iv2 = __temp;
Console::WriteLine("iv1: {0}", iv1);
Console::WriteLine("iv2: {0}", iv2);
iv2 = ++iv1;
// 次と等価:
// iv1 = IntVector::operator++(iv1);
// iv2 = iv1;
Console::WriteLine("iv1: {0}", iv1);
Console::WriteLine("iv2: {0}", iv2);
}
出力が生成されます。
iv1: [7:7:7]
iv1: [8:8:8]
iv2: [7:7:7]
iv1: [9:9:9]
iv2: [9:9:9]
標準C++ の伝統的な演算子のバージョンと違って、この演算子は直接そのオペランドの値を編集する必要はありませんし、実際に、編集するべきでもありません。]
もし、静的 operator++ や operator-- 関数の返却型が演算子が呼び出した型に代入できない場合、そのプログラムは不正です。[例:
value struct V {
static V^ operator++(V^ v) {
Console::WriteLine("V::operator++");
return v;
}
static operator V (V^ v) {
Console::WriteLine("V::operator V");
return *v;
}
};
int main() {
V v; // 変換関数が必要
++v;
V^ v2 = gcnew V;
++v2; // 変換関数は不要
}
V^ から V への暗黙の変換演算子無しに、素の値型にボックス化値型を代入する方法はありません。ゆえに、++v は v = V::operator++(v) に書き直される時、代入が診断されます。 ++v2 の場合には、v2 は V のハンドルであり、そのため、なんの変換も必要ありません。それはそのままでコンパイルされます。]
19.7.4 演算子の合成
複合代入演算子(+=, -=, *=, /=, %=, >>=, <<=, ^=, &=, |=)は他の演算子から合成されます。演算 x @= y において( @ は上にリストした演算子の一つを示しています)、もし、 operator@= の検索に成功すれば、これまでの指定されたルールが適用されます。
そうでなければ、演算 x @= y は x = x @ y として書き換えられ(その場合、標準 C++ の§5.17/7 は「 E1
op= E2 の形式の演算の振る舞いは、E1 がただ一度だけ評価されるという点を除いては、 E1 = E1
op E2 と等価です」であることを要請します。)、そして、変換された演算はこれまで指定されたルールで解釈されます。
もし、operator@= の多重定義が多重定義解決や合成後に適用されなかったなら、そのプログラムは不正です。
合成はネイティブ・クラス内で定義された演算子に起こるべきではありません。
[例:
public ref class IntVector {
...
public:
...
static IntVector^ operator+(IntVector^ iv, int i) { ... }
static IntVector^ operator+(IntVector^ iv1, IntVector^ iv2) { ... }
};
IntVector^ iv1 = gcnew IntVector(19);
iv1 += 20; // iv1 = iv1 + 20 として合成される
iv1 += iv1; // iv1 = iv1 + iv1 として合成される
]
もし、複合代入演算子の左オペランドがプロパティであれば、演算子合成は、プロパティの型が存在している複合代入演算であるとしても、常に演算書き換えが使われるべきです。
19.7.5 名前付け変換
コンパイルの間、任意の演算子関数の名前はその関数のためにソース・コード中で使われた C++ 識別子です。
例えば、追加演算子の識別子は operator+ です。
しかしながら、メタデータでは、その関数は違う名前、op_xxx の形の、を持つでしょう。
全ての演算子関数名はこの形を持ち、この節を通して表中にリストしている演算子関数名は、メタデータ中で特定の場合に予約されています。
特に、CLI クラス型中でこれらのどれかの名前を持つメンバ関数を宣言したり定義するプログラムは不正です。
CLS は CLS 消費者と生産者言語表記が同意している上で、演算子のセットを識別しています。
CLS-compliant 演算子(
§19.7.5.1 )のセットは標準 C++ (CLI 標準のパーティションI、§10.3 を参照)でサポートされている演算子のセットと重なり合います。
CLS-compliant 演算子と重ならない C++ 演算子は C++ 依存演算子(
§19.7.5.4 )として知られています。
19.7.5.1 CLS-compliant 演算子
次の全ての条件が起きた時、演算子は CLS-compliant です。
- その演算子関数は表19-1: CLS-compliant 単項演算子か表19-2: CLS-compliant 二項演算子のどちらかにリストされているものです。
- 演算子関数は ref クラスか値クラスの静的メンバです。
- もし、値クラスが演算子関数のあるパラメータか返値であれば、その値クラスは参照でもポインタやハンドルとしても通過しません。
- もし、ref クラスが演算子関数のあるパラメータか返値であれば、その ref クラスはハンドルとして受け渡されるかハンドルで返します。そのハンドルは参照として返ったり受け渡されたりするべきではありません。
もし、上記の条件にあわなければ、演算子関数は C++ 依存(
§19.7.5.4 )です。
表19-1: CLS-compliant 単項演算子
| メタデータ関数名 | C++演算子関数名 |
| op_AddressOf | operator& |
| op_LogicalNot | operator! |
| op_OnesComplement | operator~ |
| op_PointerDereference | operator* |
| op_UnaryNegation | operator- |
| op_UnaryPlus | operator+ |
表19-2: CLS-compliant 二項演算子
| メタデータ関数名 | C++演算子関数名 |
| op_Addition | operator+ |
| op_BitwiseAnd | operator& |
| op_BitwiseOr | operator| |
| op_Comma | operator, |
| op_Decrement | operator-- |
| op_Division | operator/ |
| op_Equality | operator== |
| op_ExclusiveOr | operator^ |
| op_GreaterThan | operator> |
| op_GreaterThanOrEqual | operator>= |
| op_Increment | operator++ |
| op_Inequality | operator!= |
| op_LeftShift | operator<< |
| op_LessThan | operator< |
| op_LessThanOrEqual | operator<= |
| op_LogicalAnd | operator&& |
| op_LogicalOr | operator|| |
| op_Modulus | operator% |
| op_Multiply | operator* |
| op_RightShift | operator>> |
| op_Subtraction | operator- |
19.7.5.2 非 C++ 演算子
CLS は標準 C++ がサポートしていないいくつかの演算子の名前を提供しています。[注意:他の言語のコンパイラはこれらの名前について関数に寛容であるべきではありません。それはもし、ユーザー定義関数が 追記 F でリストされた名前の一つを与えられているばあい、 C++/CLI 実装が互換性診断のために支給することを要請しています。]
表19-5: C++依存二項演算子
| メタデータ関数名 | C++ 演算子関数名 |
| op_False | なし |
| op_True | なし |
19.7.5.3 代入演算子
与えられた CLI 代入演算子は、これらの演算子によって、値によるパラメータと値を結果として返し、CLS 勧告は C++ と互換性はありません。C++ はインスタンス関数に代入演算子を要請するので、C++/CLI 実装は CLS 代入演算子(表 19-3: CLS 勧告代入演算子にリストされている)を生成し、消尽することを要請されていません。その様な、表 19-3: CLS 勧告代入演算子からの名前があるユーザー定義関数はなんの特別な扱いも受けません。
表19-3: CLS 勧告代入演算子
| メタデータ関数名 | C++演算子関数名 |
| op_Assign | 等価なものはなし |
| op_UnSignedRightShiftAssignment | 等価なものはなし |
| op_RightShiftAssignment | 等価なものはなし |
| op_MultiplicationAssignment | 等価なものはなし |
| op_SubtractionAssignment | 等価なものはなし |
| op_ExclusiveOrAssignment | 等価なものはなし |
| op_LeftShiftAssignment | 等価なものはなし |
| op_ModulusAssignment | 等価なものはなし |
| op_AdditionAssignment | 等価なものはなし |
| op_BitwiseAndAssignment | 等価なものはなし |
| op_BitwiseOrAssignment | 等価なものはなし |
| op_DivisionAssignment | 等価なものはなし |
19.7.5.4 C++依存演算子
もし、演算子関数が CLS-compliant 演算子(
§19.7.5.1 )の基準にマッチしなければ、その演算子は C++ 依存です。表19-4:C++依存単項演算子、表19-5: C++依存二項演算子はこれらの関数を識別します。(例え、これらのメタデータ名称が CLS-compliant でなくても、二つ以外の全てはCLS によって勧告されています。その二つの例外は op_FunctionCall と op_Subscript です。)
表19-4: C++依存単項演算子
| メタデータ関数名 | C++演算子関数名 |
| op_AddressOf | operator& |
| op_LogicalNot | operator! |
| op_OnesComplement | operator~ |
| op_PointerDereference | operator* |
| op_UnaryNegation | operator- |
| op_UnaryPlus | operator+ |
表19-5: C++依存二項演算子
| メタデータ関数名 | C++演算子関数名 |
| op_Addition | operator+ |
| op_AdditionAssignment | operator+= |
| op_Assign | operator= |
| op_BitWiseAnd | operator& |
| op_BitWiseAndAssignment | operator&= |
| op_BitWiseOr | operator| |
| op_BitWiseOrAssignment | operator|= |
| op_Comma | operator, |
| op_Decrement | operator-- |
| op_Division | operator/ |
| op_DivisionAssignment | operator/= |
| op_Equality | operator== |
| op_ExclusiveOr | operator^ |
| op_ExclusiveOrAssignment | operator^= |
| op_FunctionCall | operator() |
| op_GreaterThan | operator> |
| op_GreaterThanOrEqual | operator>= |
| op_Increment | operator++ |
| op_Inequality | operator!= |
| op_LeftShift | operator<< |
| op_LeftShiftAssignment | operator<<= |
| op_LessThan | operator< |
| op_LessThanOrEqual | operator<= |
| op_LogicalAnd | operator&& |
| op_LogicalOr | operator|| |
| op_MemberSelection | operator-> |
| op_Modulus | operator% |
| op_ModulusAssignment | operator%= |
| op_MultiplicationAssignment | operator*= |
| op_Multiply | operator* |
| op_PointerToMemberSelection | operator->* |
| op_RightShift | operator>> |
| op_RightShiftAssignment | operator>>= |
| op_Subscript | operator[] |
| op_Subtraction | operator- |
| op_SubtractionAssignment | operator-= |
19.8 非静的演算子
C++/CLI は標準C++ の非静的、そして、グローバル演算子をサポートをしているのだけれど、これらの演算子関数は CLS-compliant(
§19.7.5.1 )ではありません。その様な演算子は
§19.7 とその節で様々なコンテキストを議論しています。特に、多重定義候補セットを均質化すること(
§19.7.1 )、ハンドルの演算子(
§19.7.2 )、インクリメント・デクリメント演算子(
§19.7.3 )、演算子合成(
§19.7.4 )、そして、名前変換(
§19.7.5 )。
[注意:上層型認識(
§12.4 )は最上位型にのみ適用され、最上位関数には適用されません。そのため、グローバル演算子関数はその親アセンブリの外側で視認することができません。しかしながら、非静的メンバ関数として実装された演算子はその親アセンブリの外側から視認されることができます。]
演算子 new と delete は CLI クラス型のために多重定義するべきではありません。
メタデータの詳細は、
§34.7.8 を参照のこと。
19.9 インスタンス・コンストラクタ
C++/CLI は静的コンストラクタ表記を追加しているので、標準C++ で使われていた"コンストラクタ"という全ての用語の使用は、C++/CLI では
instance constructor (インスタンス・コンストラクタ) として参照します。
C++ のネイティブ・クラスのコンストラクタはオブジェクト・コンストラクタから仮想関数を呼び出す振る舞いは、派生型(標準C++ §12.7 参照)ではなくその基底クラスのものや、コンストラクタ下でクラスの仮想関数を呼び出す結果となります。ref クラスのコンストラクタから呼ばれる仮想関数の振る舞いは常にそのもっとも派生したクラスから適切に仮想関数呼び出しを行います。
ref クラスのコンストラクタは次の順で実行します。
- 宣言した順にクラスの全てのメンバを初期化します。
- 基底クラスのコンストラクタを呼び出します。
- ユーザーが記述したコンストラクタの本文を実行します。
もし、例外がクラス・メンバの初期化の間に発生したら、それぞれの完全に生成されたメンバのデストラクタは宣言された順の逆に呼び出され、クラスのファイナライザが、もし存在するなら、呼び出されます。
もし、例外が基底クラスのコンストラクタの間に発生したら、それぞれの完全に生成されたメンバのデストラクタは宣言された順の逆に呼び出され、そして、クラスのファイナライザが、もし存在するなら、呼び出されます。
もし、ユーザー記述のコンストラクタの本文で例外が起きたのであれば、Dispose(true) 関数を基底クラスのデストラクタ(
§34.7.13.7 参照)で呼び出されるのと同じ振る舞いで、基底クラスは破棄されます。基底クラスのクリーン・アップの後、各々のメンバのデストラクタが宣言順の逆に呼び出され、もし存在するなら、ファイナライザが呼び出されるでしょう。
メタデータの詳細は、
§34.7.9 を参照のこと。
19.10 静的コンストラクタ
static constructor (静的コンストラクタ) は ref クラスや値クラスの静的データ・メンバを初期化するために必要とされるアクションを実装した関数メンバです。静的コンストラクタは、ストレージ・クラスと static を指定する形式であるという点を除いて、標準 C++ のインスタンス・コンストラクタ(§8.4 )とちょうど似たように宣言されます。
静的コンストラクタは
ctor-initializer を持つべきではありません。
静的コンストラクタは継承されず、直接呼び出すこともできません。
クラスの静的コンストラクタは CLI 標準、パーティションII で指定されているように実行されます。
もし、クラスが初期化子に任意の静的フィールド(initonly フィールドを含みます)を持っていたら、それらのフィールドは直ちに静的コンストラクタが実行されるよりも先に、それらが宣言された順に初期化されます。
[例:コード、
ref struct A {
static A() {
cout << "Init A" << "\n";
}
static void F() {
cout << "A::F" << "\n";
}
};
ref struct B : A {
static B() {
cout << Init B" << "\n";
}
static void F() {
cout << "B::F" << "\n";
}
};
int main() {
A::F();
B::F();
}
は以下のひとつの出力を生成します。
Init A Init A Init B
A::F Init B Init A
Init B A::F A::F
B::F B::F B::F
A の静的コンストラクタは A の任意の静的メンバにアクセスする前に実行されるべきであり、そして、B の静的コンストラクタは B の任意の静的メンバにアクセスする前に実行されるべきであり、そして、A::F は B::F の前に呼び出されるべきだからです。]
静的コンストラクタは親クラスの外側に、static の前置詞がまた存在する違いを除いて、インスタンス・コンストラクタのクラス外に対応した同じ構文を使って定義されることが可能です。[例:
ref class R {
public:
static R(): // 静的コンストラクタ宣言
R(); // インスタンス・コンストラクタ宣言
R(int) { ... } // インライン・インスタンス・コンストラクタ宣言
};
static R::R() { ... } // クラス外静的コンストラクタ定義
R::R() { ... } // クラス外インスタンス・コンストラクタ宣言
]
[注意:標準 C++ では、コンストラクタのクラス外定義は内部リンケージを持つため許可されていません。それは、static と宣言されることは許されていないと言うことです。]
静的コンストラクタは private の
access-specifier (アクセス指定子)を持つべきです。
もし、ref ないし、値クラスがユーザー定義静的コンストラクタを持っていなければ、
default static constructor (デフォルト静的コンストラクタ) が暗黙のうちに定義されます。それは初期化のセットとしてそのクラスのユーザー定義コンストラクタが空の関数本文を持って実行されているかのように実行します。
メタデータの詳細は、
§34.7.10 を参照のこと。
19.11 リテラル・フィールド
literal field (リテラル・フィールド) はリテラル・フィールドの型とその初期化子の値を持つ名前付きコンパイル時定数右辺値です。リテラル・フィールドを協調的に追加するために、標準 C++ (§9.2 )のクラス・メンバ宣言構文の生成物のひとつはメンバ宣言が
initonly-or-literal 識別子 literal (
§19.1 )を含むことができるよう拡張されています。
リテラル・フィールド宣言の
member-declarator-list (メンバ宣言子リスト) のそれぞれの
member-declarator (メンバ宣言子) は
constant-initializer (定数初期化子) を含むべきです。
リテラル・フィールドは静的メンバのようにアクセスすることができますが、リテラル・フィールド定義は static キーワードを含みません。
コンパイラがリテラル・フィールドの正規な使用を通した時はいつでも、コンパイラはリテラル・フィールドが関連する値に置き換えるべきです。
リテラル・フィールドはスカラー型を持つべきです。[注意:これはハンドル型を含みます。]しかしながら、
member-declarator (メンバ宣言子) 中の
decl-specifier-seq は
cv-qualifier を含むべきではありません。
constant-initializer (定数初期化子) 中の
constant-expression (定数演算) は目標の型の値か、標準変換シーケンスによって目標の型に変換できる型の値を生じるべきです。
[注意:
constant-expression (定数演算) はコンパイル時に完全に評価ができる演算です。System::String^ に gcnew 演算子を適用すること以外にハンドル型の非ヌル値を作るただ一つの方法なので、そして、その演算子は
constant-expression (定数演算) 中では許されていないので、System::String^ 以外のハンドル型のリテラル・フィールドに唯一許されている値は nullptr です。]
定数値にシンボル名称が望まれる時、しかし、その値の型がリテラル・フィールド宣言で許されていない時、ないし、その値がコンパイル時に
constant-expression (定数演算) によって計算できない時、initonly フィールド(
§19.12 )が代わりに使われることが可能です。
リテラル・フィールドは依存性が循環しない限り同じプログラム中で他のリテラル・フィールドに依存することが許されています。
[例:
ref struct X {
literal double PI = 3.1415926;
literal int MIN = -5, MAX = 5;
literal int COUNT = MAX - MIN + 1;
literal int Size = 10;
enum class Color { red, white, blue};
literal Color DefaultColor = Color::red;
};
int main() {
double radius;
cout << "Enter a radius: ";
cin >> radius;
cout << "Area = " << X::PI * radius * radius << "\n";
static double d = X::PI;
for (int i = X::MIN; i <= X::MAX; ++i) { ... }
float f[X::Size];
}
]
リテラル・フィールドのバージョン化についての議論は
§19.12.2 を参照のこと。
メタデータの詳細は、
§34.7.11 を参照のこと。
19.12 initonly (初期化時のみ)フィールド
initonly フィールドを協調的に追加するために、標準C++ (§9.2 )の構文的クラス・
member-declaration (メンバ宣言) の生成物の一つとしてメンバ宣言に
initonly-or-literal 識別子 initonly (
§19.1 )を含むことができるよう拡張されており、それによってできたメンバを
initonly field とします。
initonly フィールドの初期化はその定義の一部としてのみ発生します。任意の initonly フィールドへの代入(代入演算子や前置・後値インクリメント・デクリメント演算子を通して)はそのフィールドのクラスのインスタンス・コンストラクタないし、静的コンストラクタでのみ発生します。[注意:もちろん、その様な代入はコンストラクタの
ctor-initializer を通してなされます。]initonly フィールドの初期化、と、への代入、は次のコンテキストでのみ許されます。
- 静的 initonly フィールドのために、initonly フィールドの member-declarator (メンバ宣言) の constant-initializer(定数初期化子) 中で、
- インスタンス・フィールドには、initonly フィールド定義を含むクラスのインスタンス・コンストラクタにおいて。静的フィールドには、initonly フィールド定義を含むクラスの静的コンストラクタにおいて。
任意のそのほかのコンテキストにおいて initonly フィールドに代入しようという試みや、フィールドのアドレスを取得しようとしたり、任意のコンテキスト中にその参照を拘束しようとするプログラムは不正です。
initonly フィールドの型は ref クラスであるべきではありません。
[例:
public ref class R {
initonly static int svar1 = 1; // OK
initonly static int svar2; // エラー:ここで初期化されなければならない。もしくは、
// 静的コンストラクタで代入されること
initonly static int svar3; // OK 静的コンストラクタ中で代入されている
initonly int mvar1 = 1; // エラー、初期化は静的でなければならない
initonly int mvar2;
initonly int mvar3;
public:
static R() {
svar3 = 3;
svar1 = 4; // OK しかし、値 1 を上書きする
smf2();
}
static void smf1() {
svar3 = 5; // エラー:静的コンストラクタ中でない
}
static void smf2() {
svar2 = 5; // エラー:静的コンストラクタ中でない
}
R() : mvar2(2) { // OK
mvar3 = 3; // OK
mf1();
}
void mf1() {
mvar3 = 5; // エラー:インスタンス・コンストラクタ中でない
}
void mf2() {
mvar2 = 5; // エラー:インスタンス・コンストラクタ中でない
}
};
]
ある静的 initonly フィールドは明示的にもう一つの値を使って初期化されることができ、その様なフィールドは文脈上のソースの順に初期化され、静的コンストラクタ中の任意のコードの実行前に初期化されます。
メタデータの詳細は、
§34.7.12 を参照のこと。
19.12.1 static initonlyフィールドを定数として使う
static initonly フィールドはシンボル名に定数値を望む時にとても便利ですが、値の型が literal 宣言で許されない時や、値がコンパイル時に算出できない時にはそうではありません。
19.12.2 リテラル・フィールドと static initonly フィールドのバージョン化
リテラル・フィールドと initonly フィールドは異なるバイナリ・バージョン化構文を持ちます。演算がリテラル・フィールドを参照する時にはメンバの値はコンパイル時に含まれますが、演算が initonly フィールドを参照する時には、メンバの値は実行時までは得られません。[例:次のようなソースのアプリケーションについて考えると、
namespace Program1 {
public ref struct Utils
{
static initonly int X = 1;
literal int Y = 1;
};
}
namespace Program2 {
int main() {
Console::WriteLine(Program1::Utils::X);
Console::WriteLine(Program1::Utils::Y);
}
}
Program1 と Program2 名前空間はコンパイル時には、別々に分かれている二つのソース・ファイルを意味しており、それぞれ自身のアセンブリを生成しています。Program1::Utils::X は静的 initonly フィールドとして宣言されているので、Console::WriteLine で出力される値はコンパイル時には知られておりませんが、実行時には得られることになります。故に、もし、X の値が変更され、Program1 が再コンパイルされたら、Console::WriteLine は例え Program2 が再コンパイルされていなくても、新しい値を出力するでしょう。しかしながら、Y はリテラル・フィールドなので、Y の値は Program2 がコンパイルされた時点で含まれており、Program2 が再コンパイルされるまでは Program1 の変更は影響なく残されます。]
19.13 デストラクタとファイナライザ
任意のネイティブ・クラスやrefクラスはユーザー定義デストラクタを持つことができます。そのようなデストラクタは標準 C++ によって規定されている時点に実行されます。
- スタック上に領域確保された任意の型のオブジェクトはそのオブジェクトがスコープから外れた時点で破壊されます。
- 静的ストレージに領域確保された任意の型のオブジェクトはプログラム終了時に破壊されます。
- new を使ってネイティブ・ヒープ上に領域確保されたオブジェクトは、そのオブジェクトのポインタに delete が実行された時点で破壊されます。
- gcnew を使って CLI ヒープ上に領域確保されたオブジェクトは、そのオブジェクトのハンドルに delete が実行された時点で破壊されます。
- 他のオブジェクトのメンバであるオブジェクトは終了中のオブジェクトのデストラクションの一部として破壊されます。
デストラクションの目的で、ネイティブ、そして、CLI ヒープは同等に扱われます。その二つのヒープのただ一つの違いは、自動化とメモリ再利用のタイミングだけです。ネイティブ・ヒープの場合、メモリは delete と同時に手動で回収され、CLI ヒープの場合には、メモリは delete があろうとなかろうとガベージ・コレクションの間に自動的に回収されます。加えて、CLI ヒープ上のオブジェクトは、もし、ファイナライザが存在していれば、ファイナライズされます。
メタデータの詳細は、
§34.7.13 を参照のこと。
19.13.1 デストラクタ
ref クラスのデストラクタは標準C++(12.4)と同様に定義されています。
ref クラスは直接的に定義をするか、コンパイラによって生成される、System::IDisposable インターフェイスを実装した型のデータ・メンバを一つ以上組み込んだクラスで後に発生する、デストラクタを持ちます。
ref クラス中のデストラクタの
access-specifier (アクセス指定子) は無視されます。
ref クラスのデストラクタは随意に virtual 宣言することができますが、なんの効果も持ちません。
ref クラスのデストラクタはなんの
function-modifiers (関数修飾子) (
§19.4 )を持つべきではなく、static 宣言をされるべきでもありません。
ref クラス・オブジェクトのデストラクションは次の時に始まります。:
- オブジェクトは自動ストレージ期間を持ち、オブジェクトがスコープ外に外れた。
- オブジェクトは囲うクラスのメンバとして組み込まれ、その囲うクラスのデストラクタを実行する。
- オブジェクトがコンストラクション下にあり、コンストラクタが完遂する前に例外を投げられる。
- オブジェクトを示すハンドルに delete キーワードが適用される。[注意:もし、ハンドルが nullptr を値として持っていたなら、デストラクションが始まります。が、何も行いません。]
- オブジェクトのデストラクタ関数がプログラマによって明示的に呼び出される。(これは修飾名を使用して特定の基底クラスのデストラクタ関数を呼んだ場合も含みます。)
コンストラクションを完遂したオブジェクトに(コンストラクタからなんの例外も投げられていない)、デストラクションは常に System::IDisposable::Dispose 関数を通して呼び出されることによって始まります。
例外を投げたコンストラクタから呼び出されるデストラクタの振る舞いについては、
§19.9 を参照してください。デストラクション後の ref クラス・オブジェクトのメンバへのアクセスは不正ですが、なんの検証も勧告されません。[注意:デストラクション後の ref クラス・オブジェクトへのメンバ・アクセスは ref クラス制作者の制御下にあります。制作者はメンバがデストラクション後も使用可能かどうかドキュメント化するべきです。]
コンストラクタ同様、ref クラスのデストラクタ中の仮想関数呼び出しはオブジェクトのもっとも期待している派生クラスから適切な仮想関数が呼び出される結果となります。
メタデータの詳細は、
§34.7.13.2 を参照のこと。
19.13.2 ファイナライザ
標準 C++ スタイルのデストラクタを通した決定的クリーン・アップを提供しているのと同様に、C++/CLI は ref クラスのインスタンスがもはや参照されなくなった時の、非決定的クリーン・アップのメカニズムを提供します。このメカニズムのことを
finalizer (ファイナライザ) と呼びます。
随意な
function-specifier (関数指定子) に ! を続け、ファイナライザのクラス名に空のパラメータ・リストを使って特殊な宣言構文が ref クラス定義中のファイナライザを宣言するために使われます。その様な宣言において、ファイナライザのクラス名が続く ! は随意に括弧で囲むことができますが、その様な括弧は無視されます。
typedef-name はファイナライザ宣言のために宣言中に ! に続く
class-name として使われるべきではありません。
ファイナライザはそのクラス型のオブジェクトの終了処理のために使われます。
ファイナライザはパラメータを取らず、それになんの返却型も指定できません(void ですらも)。
ファイナライザのアドレスは取得されるべきではありません。
ファイナライザは任意の
function-modifiers (関数修飾子)(
§19.4 )も static や virtual 宣言も持つべきではありません。
ファイナライザは const、volatile、ないし、const volatile オブジェクトで呼び出されることができます。
ファイナライザは const、volatile、ないし、const volatile 宣言されるべきではありません。
const と volatile 構文はファイナライズされているオブジェクトに適用されません。
それらは大多数の派生オブジェクトがファイナライズを始めた時、その効果をやめます。
ref クラス中のファイナライザの
access-specifier (アクセス修飾子) は無視されます。
任意の ref クラスはユーザー定義ファイナライザを持つことができます。ファイナライザは、CLI で指定されているガベージ・コレクタによって0回以上実行されます。
任意の ref クラス T 中のファイナライザ関数はその同じクラス中にある他の関数からのみ呼ぶことができるべきです。ファイナライザへの呼び出しは基底クラスのファイナライザの実行を結果としないべきです。
メタデータの詳細は、
§34.7.13.3 を参照のこと。