MikoScript3 言語仕様
演算子の多重定義
本言語では、クラスのインスタンスだけではなく、クラス自身も、或いは、構造体も、
オブジェクトであって、これらは「複合箱」によって実現されているということは、既に
述べましたが、複合箱に対する演算子の機能は、あらかじめ確定しているものもあれば、
確定していないものもあります。複合箱に対する機能が確定していない演算子を複合箱に
適用すれば、例外が発生します。
しかし、本言語では、各種の複合箱に対して、演算子の機能を定義できるようになって
います。これを「演算子の多重定義」と言います。多重定義というのは、同一の演算子に
対して、各種のオペランドに応じて異なる機能を個別に定義できるということです。
複合箱に対して、演算子の機能を定義をしておけば、その複合箱を演算子で操作できる
ので、関数コールで操作するよりも、直感的に分かり易く簡潔になることもよくあります。
●演算子関数
本言語では、「演算子の多重定義」は「演算子関数」の定義によって行ないます。この
演算子関数では、演算子の機能を、演算子とオペランドとの関係によって定義します。
例えば、次のような3次元座標空間のベクトルを表わす Vector というクラスがある
とします。
class ::Vector
{
function Construct( a, b, c ) { .x = a; .y = b; .z = c; }
}
このクラスのオブジェクトに対する加算演算子 + を定義するには、次のようにします。
operator Vector A + Vector B
{
return Vector( A.x + B.x, A.y + B.y, A.z + B.z );
}
ここで、operator は、演算子関数の定義の開始を示す予約語です。その次に、演算子と
オペランドとの演算関係を記述します。加算二項演算子 + の一般形式は、
A + B
ですが、ここでのオペランドは、Vector クラスなので、この演算関係は、
Vector + Vector
となります。この各項は、演算子関数の引数としての役割を担います。そのため、引数名
を追加して、
Vector A + Vector B
となります。この後に続く波括弧 { } のブロック内には、その演算子の機能を実現する
処理内容を記述します。ここでは、
return Vector( A.x + B.x, A.y + B.y, A.z + B.z );
ですが、これは、引数の Vector A と Vector B の各要素の加算値を要素とする Vector
クラスのインスタンスを生成して返しています。なお、演算子関数においても、その引数
と返値の受け渡しは、一般の関数とまったく同じです。従って、ここでの引数 A と B に
は、通常、対象の Vector クラスのオブジェクトへの参照が設定されます。また、ここで
返されるのは、生成された Vector クラスのインスタンスの実体になります。
定義された演算子関数は、演算子関数専用のスコープに登録されます。このスコープは、
他のどのスコープにも従属しない独立した1つのシステムスコープで、本言語システムの
開始から終了まで存続します。このスコープは、どのスレッドのどの関数からでも共通に
アクセスできます。
次に、Vector クラスの加算代入演算子 += の定義例を、以下に示します。
operator Vector A += Vector B
{
A.x += B.x; A.y += B.y; A.z += B.z;
return A;
}
この演算関係の指定では、前の場合と比較して、二項演算子が + から += に代っている
だけです。ここでの処理内容は、引数の Vector A の各要素に Vector B の各要素の値を
加算代入しています。前述の通り、引数 A は対象の Vector クラスのオブジェクトへの
参照になるので、この加算代入では、その対象のオブジェクト自身の値が更新されます。
そして、その対象のオブジェクトの実体を示す値が返されます。なお、この返値は、その
対象のオブジェクトへの参照ではありません。関数の返値に関する詳細は「関数」の章の
・関数の返値と return 文
の節で述べています。
次に、Vector クラスのオブジェクトと一般の数値との乗算を行なう演算子 * を、定義
する例を、以下に示します。
operator Vector V * K
{
V.x *= K; V.y *= K; V.z *= K;
return V;
}
ここでの演算関係の指定では、二項演算子が * になっていて、その右辺は引数名だけで、
クラス名の指定がありません。これは、右辺の対象がクラスではないことを意味します。
ここでは、右辺の対象が一般の数値なので、クラス名が無く、引数名だけになっています。
ここでの処理内容は、引数の Vector V の各要素を K 倍して、その V の参照先の実体を
示す値を返すようになっています。
今までに定義した演算子の機能を使うと、次のようなことができます。
A = Vector( 1, 2, 3 );
B = Vector( 10, 20, 30 );
C = Vector( 100, 200, 300 );
P = A + B; // ベクトル A と B を加算して P に代入
Q = ( A + B + C ) * 20; // ベクトル A, B, C を加算して 20 倍して P に代入
R = ( C += B ); // ベクトル C に B を加算代入して、それを R に代入
print "P: " : P.x, P.y, P.z;
print "Q: " : Q.x, Q.y, Q.z;
print "R: " : R.x, R.y, R.z;
これを実行すると、次の通り、プリントされます。
P: 11, 22, 33
Q: 2220, 4440, 6660
R: 110, 220, 330
なお、演算子関数は、このように、演算子とオペランドとの関係によって定義しますが、
対象となるクラスのメンバー関数としては定義できません。例えば、
class ::Vector
{
operator + ( Vector V )
{
return Vector( .x + V.x, .y + V.y, .z + V.z );
}
}
とできてもよさそうですが、そもそも本言語の関数定義の引数には型情報を付けません。
そのため、上記の演算子関数のメンバー関数としては、
operator + ( V )
となり、引数 V は任意の型を対象にすることになります。従って、Vector + Vector の
演算以外にも、Vector + (他の型) の演算が必要になった場合には、このメンバー関数内
に混合して定義しなければならないということになります。この制約もさることながら、
演算子関数をクラスのメンバー関数として定義する場合の書式は、感覚的に分かりにくい
表記になってしまいます。また、++ 演算子のように前置と後置がある場合、定義書式を
どう区別するかも問題になります。ちなみに、このような制約や問題は、本言語の演算子
関数の定義書式では、次のように、解消されています。
operator Vector A + OtherType B { ・・・・・ }
operator ++ Vector V { ・・・・・ }
operator Vector V ++ { ・・・・・ }
●二項演算子の多重定義
二項演算子の多重定義の書式は、前節であらかた説明しましたが、一般的には、次の
ようになります。
operator 左項 二項演算子 右項
{
演算子関数の定義本体
}
ここで、「左項」と「右項」には、その二項演算子が対象とするオペランドを規定します。
これは、
型名 引数名
になるか、あるいは、
引数名
だけになります。「型名」には、対象のオペランドのクラス名、または、構造体名を規定
します。これは、単一の識別名で、スコープ規定演算子等は付きません。「型名」が省略
された場合、その対象はクラスや構造体のオブジェクトではないことを示します。つまり、
非オブジェクト(定数、単一箱、オブジェクトではない「箱を入れる箱」)が対象になる
ことを示します。「引数名」には、その演算子関数の引数の識別名を規定します。これは
省略できません。また、左右両項で同名になってはいけません。
上記書式内の「二項演算子」には、定義対象の二項演算子の表記そのものを記述します。
本言語では、全ての二項演算子が多重定義可能というわけではなく、次の二項演算子
だけが、多重定義可能です。
・算術演算子 + - * / %
・ビット操作演算子 | & ^ << >>
・大小比較演算子 > < >= <=
・等価性比較演算子 == !=
・演算併用代入演算子 += -= *= /= %= &= |= ^= <<= >>=
二項演算子の多重定義の基本的な例は、前節で示しましたので、ここでは、やや応用的
な例として、C++ 言語風に標準出力を行なう cout クラスと << 演算子を作成してみます。
まず、cout クラスは、次のようになります。
class ::cout {}
このクラスは、グローバルスコープに cout という名前で登録されますが、必要なのは、
その存在だけで、中身は不要です。このクラスはそれ自身をオブジェクトとして使うので、
そのインスタンスは生成しません。
次に、cout クラスに対する << 演算子の機能を定義します。その場合、この演算子の
左項は cout クラスで、右項は標準出力の対象になります。標準出力の対象にはいろいろ
ありますが、非オブジェクトを対象とする演算子関数の定義は、以下のようになります。
operator cout C << X
{
print X : -;
return C;
}
ここで、引数 X の対象は非オブジェクトなので、これには型名が付いていません。この
値をどのように標準出力するかは、print 文に委任しています。この関数は、return 文
で、クラス cout を返していますが、これよって、このクラスの複製やそのインスタンス
が生成されることはありません。単にそのクラスの実体を示す値が、返されるだけです。
これについては、既に述べた通りです。この関数の返値をこのようにすることによって、
cout << A << B << C;
のように、<< 演算子を連続して、適用できるようになります。なぜなら、<< 演算子は、
左結合なので、上式の評価は、
((( cout << A ) << B ) << C );
として行なわれることになりますが、最初の cout << A の結果は cout なので、その次
は、cout << B が行なわれることになり、その結果は、cout なので、最後に cout << C
が行なわれることになります。
今度は、右項の標準出力の対象が、前出のクラス Vector のオブジェクトになる場合の
演算子関数の定義を、次に示します。
operator cout C << Vector V
{
print "( " : V.x, V.y, V.z : " )" : -;
return C;
}
このように、クラス cout に対する << の演算子関数を定義しておくと、例えば、次の
ようなことができるようになります。
x = 0;
A = Vector( 1, 2, 3 );
cout << "x = " << x << "\n";
cout << "A = " << A << "\n";
これを実行すると、次の通り、プリントされます。
x = 0
A = ( 1, 2, 3 )
ほかのクラスについても、Vector クラスの場合と同様に、cout クラスとそのクラスを
対象として << の演算子関数を定義しておけば、そのクラス専用の内容が標準出力できる
ようになります。
●前置演算子の多重定義
前置演算子の多重定義の書式は、次のようになります。
operator 前置演算子 項
{
演算子関数の定義本体
}
ここで、「前置演算子」には、定義対象の前置演算子の表記そのものを記述します。
「項」には、その前置演算子が対象とするオペランドを規定します。これは、二項演算子
の「左項」や「右項」と同様で、
型名 引数名
になるか、あるいは、
引数名
だけになります。「型名」には、対象のオペランドのクラス名、または、構造体名を規定
します。これは、単一の識別名で、スコープ規定演算子等は付きません。「型名」が省略
された場合、その対象はクラスや構造体のオブジェクトではないことを示します。つまり、
非オブジェクト箱か定数が対象になることを示します。「引数名」には、その演算子関数
の引数の識別名を規定します。これは省略できません。
本言語では、全ての前置演算子が多重定義可能というわけではなく、次の前置演算子
だけが、多重定義可能です。
・符号演算子 + -
・ビット操作演算子 ~
・増減演算子 ++ --
次に、前置演算子の多重定義例を示します。ここでは、まず、前出のクラス Vector の
各要素の符号を反転する演算子 - を定義します。
operator - Vector V
{
return Vector( - V.x, - V.y, - V.z );
}
これは、例えば、次のようにして使えます。
A = Vector( 1, 2, 3 );
B = -A; // ベクトル B の各要素は、( -1, -2, -3 )
今度は、対象のクラスをその内容を示す文字列に変換する演算子 ~ を定義してみます。
例えば、前出のクラス Vector を対象とする場合は、次のようになります。
operator ~ Vector V
{
return ##( ${ V.x }, ${ V.y }, ${ V.z } )##;
}
これを使って、例えば、次のようなことができます。
P = Vector( 1.23, -4.56, 7.89 );
print "P = " : ~P;
これを実行すると、次の通り、プリントされます。
P = ( 1.23, -4.56, 7.89 )
●後置演算子の多重定義
後置演算子の多重定義の書式は、次のようになります。
operator 項 後置演算子
{
演算子関数の定義本体
}
ここで、「項」には、その後置演算子が対象とするオペランドを規定します。この「項」
は、前置演算子の「項」と同様です。また、「後置演算子」には、定義対象の後置演算子
の表記そのものを記述します。
後置演算子には、
・増減演算子 ++ --
・真偽判定演算子 ?
がありますが、これらは全て多重定義可能です。
なお、真偽判定演算子 ? は、「項」に対する真偽判定を行なう関数を定義するための
書式上の表記で、実際に演算子として使えるわけではありません。この ? は、真偽判定
による二者択一の三項演算子 ? : の ? ではありません。
真偽判定は、if 文、while 文、for 文、論理演算( ! && || )等で行なわれますが、
複合箱に対する真偽判定は、デフォールトで、必ず「真」になってしまいます。しかし、
真偽判定演算子 ? の演算子関数を定義すれば、その「項」に対する真偽判定を、個別に
行なわせることができるようになります。
例えば、前出のクラス Vector に対する真偽判定の定義は、次のようになります。
operator Vector V ?
{
return V.x && V.y && V.z ;
}
これは、零ベクトル( 全要素が 0 )の時に「偽」、それ以外の時に「真」と判定します。
この関数は、Vector オブジェクトに対する真偽判定が行なわれる際には必ず呼ばれて、
その返値がその真偽判定結果に反映されます。例えば、
A = Vector( 0, 0, 0 );
B = Vector( 1, 1, 1 );
if( A ) print "A == TRUE"; else print "A == FALSE";
if( B ) print "B == TRUE"; else print "B == FALSE";
print "! A == " : ! A ? "TRUE" : "FALSE" ;
print "A && B == " : A && B ? "TRUE" : "FALSE" ;
print "A || B == " : A || B ? "TRUE" : "FALSE" ;
を実行すると、次の通り、プリントされます。
A == FALSE
B == TRUE
! A == TRUE
A && B == FALSE
A || B == TRUE
真偽判定演算子 ? の演算子関数の定義で「項」に型名がない場合、例えば、
operator X ?
{
・・・・・
}
では、真偽判定の対象が複合箱(非オブジェクト)の場合に限り、この演算子関数が呼ば
れます。真偽判定の対象が基本データ型や単一箱の場合は、この演算子関数は呼ばれずに、
デフォールトの真偽判定が行なわれます。
真偽判定演算子 ? の演算子関数の返値は、通常、1 か 0 にしますが、どのような値で
あっても、それに対してデフォールトの真偽判定が行なわれます。例えば、返値が null
の場合は「偽」と判定されます。また、返値が複合箱の場合は、それに対する真偽判定の
演算子関数が定義されていても、それはコールされずに、「真」と判定されます。
●制限事項
本言語では、次の演算子は、多重定義できません。
・スコープ規定演算子 :: . ^ $ @
・括弧演算子 () []
・代入演算子 = := <-
・論理演算子 ! && ||
・真偽による二者択一演算子 ? :
・リレー型関数コール演算子 '
・代入型関数コール演算子 :==
・ラベル名演算子 :
これらの演算子が多重定義できない理由は、次の通りです。
・スコープ規定演算子、括弧演算子、代入演算子、ラベル名演算子は、全種の箱に対して
確定した機能があり、これを変更可能にした場合、いろいろなところで矛盾や不整合が
生じてしまいます。あるいは、それが生じないようにして多重定義可能にしたとしても、
そのためのオーバーヘッドが無視できなくなります。そこまでして実現するだけの価値
は見いだせません。
・リレー型関数コール演算子、代入型関数コール演算子は、関数コール形式の構文に関連
した演算子で、そのオペランドは、通常の多重定義の対象にはなりません。
・論理演算子の && と || は、左項と右項の両方を評価してから演算を行なうのではなく、
左項の評価結果によっては、右項を評価しない場合もあります。この論理演算子を多重
定義した場合、このような処理はできません。両項とも評価してからその演算子関数を
コールすることになります。この論理演算子に対して、元来の処理をするのか、演算子
関数をコールするのかでは、かなりの差があるため、この差を実行に吸収するのは困難
です。そのため、それはコンパイル時に確定している必要がありますが、本言語では、
実行時に動的に変化するので、それも無理です。
・本言語では、真偽判定演算子の多重定義ができるようなっているので、各論理演算子を
個別に多重定義できなくても、特に不便はないし、むしろその方がまとめて論理演算を
定義できるので便利な場合もあり、if 文等での条件判定にも反映されるので、敢えて、
否定論理演算子 ! や真偽による二者択一演算子 ? : でも、個別には多重定義できない
ようにしています。
多重定義可能な演算子においても、その演算子の優先順位、結合性、項の配置や数は、
変更できません。また、新たな演算子の追加もできません。
多重定義可能な二項演算子において、非複合箱(単一箱または定数)項どうしに対する
機能は変更できません。また、多重定義可能な単項演算子において、非複合箱項に対する
機能は変更できません。例えば、
operator X + Y { ・・・・・ }
のように定義された演算子関数は、左項と右項のどちらかが、非オブジェクトの複合箱で
あれば、呼ばれますが、どちらも基本データ型の場合には、決して呼ばれません。
演算子関数の定義で、型名が指定されている項は、その型名のクラス、構造体、または、
そのインスタンスが対象になりますが、その基底クラスや派生クラスは対象になりません。
●補足説明
演算子関数の返値がオブジェクトの場合に、そのメンバー関数をコールするには、その
返値を生成する式を、角括弧 [ ] で囲う必要があります。
例えば、前出のクラス Vector に、以下のように、メンバー関数を追加して、これを、
Vector どうしの加算演算の結果に適用すると、
function ::Vector.Show() { print .x, .y, .z; }
[ Vector( -1, -2, -3 ) + Vector( 11, 22, 33 ) ].Show();
となります。これを実行すると、次の通りプリントされます。
10, 20, 30
なお、本言語の文法上、次のようにはできません。
( Vector( -1, -2, -3 ) + Vector( 11, 22, 33 ) ).Show();