演算子オーバーロード
オーバーロードは、クラスや構造体の特別な名前のメンバ関数を単項/二項演算子の 実装であると解釈することで実現しています。 新しく文法の追加はありません。
単項演算子のオーバーロード
演算子 | 関数名 |
---|---|
-e | opNeg |
+e | opPos |
~e | opCom |
e++ | opPostInc |
e-- | opPostDec |
cast(type)e | opCast |
オーバーロード可能な単項演算子 op と、 対応するクラスや構造体のメンバ関数名 opfunc について、次の式は:
op a
a をクラスか構造体オブジェクトへの参照として、 あたかも次のコードを書いたかのように解釈されます:
a.opfunc()
++e と --e のオーバーロード
++e は意味論的に (e += 1) と同等と定義されているので、 ++e は (e += 1) と書き直してから、 オーバーロードが定義されていないか調べます。 --e についても同様です。
例
class A { int opNeg(); } A a; -a; // a.opNeg() と同等
class A { int opNeg(int i); } A a; -a; // a.opNeg() と同等。エラー。
cast(type)e のオーバーロード
メンバ関数 e.opCast() が呼び出され、 opCast() の返値がtype型へと暗黙に変換されます。 返値型のみが違う関数をオーバーロードさせることはできないので、 一つの構造体やクラスにつき、 opCast は一つまでです。 キャスト演算子の追加は、暗黙のキャストには影響しません。 明示的キャストの時のみ適用されます。
struct A { int opCast() { return 28; } } void test() { A a; long i = cast(long)a; // i は 28L になる void* p = cast(void*)a; // エラー。intからvoid*への // 暗黙変換はできない int j = a; // エラー。Aからintへの // 暗黙変換はできない }
二項演算子のオーバーロード
演算子 | 可換? | 関数名 | _r関数名 |
---|---|---|---|
+ | yes | opAdd | opAdd_r |
- | no | opSub | opSub_r |
* | yes | opMul | opMul_r |
/ | no | opDiv | opDiv_r |
% | no | opMod | opMod_r |
& | yes | opAnd | opAnd_r |
| | yes | opOr | opOr_r |
^ | yes | opXor | opXor_r |
<< | no | opShl | opShl_r |
>> | no | opShr | opShr_r |
>>> | no | opUShr | opUShr_r |
~ | no | opCat | opCat_r |
== | yes | opEquals | - |
!= | yes | opEquals | - |
< | yes | opCmp | - |
<= | yes | opCmp | - |
> | yes | opCmp | - |
>= | yes | opCmp | - |
+= | no | opAddAssign | - |
-= | no | opSubAssign | - |
*= | no | opMulAssign | - |
/= | no | opDivAssign | - |
%= | no | opModAssign | - |
&= | no | opAndAssign | - |
|= | no | opOrAssign | - |
^= | no | opXorAssign | - |
<<= | no | opShlAssign | - |
>>= | no | opShrAssign | - |
>>>= | no | opUShrAssign | - |
~= | no | opCatAssign | - |
in | no | opIn | opIn_r |
オーバーロード可能な二項演算子 op と、 対応するクラスや構造体のメンバ関数名 opfunc, 及び opfunc_r、 そして次の式:
a op bについて、 次の一連の規則によってどの形に解釈されるかが決定します:
- この式は、存在する全ての a.opfunc や b.opfunc_r 関数を使って、次の形に書き換えられます:
a.opfunc(b) b.opfunc_r(a)
そして、それら全てのオーバーロードと見なして、 最適なものを実際に適用します。 a.opfunc と b.opfunc_r のどちらかが存在するのにもかかわらず、 引数とマッチするものがない場合はエラーになります。 - 演算子が可換ならば、
次の形の適用が可能かどうかまで調べます:
a.opfunc_r(b) b.opfunc(a)
- それでも適切な関数が見つからない場合、もし a か b が構造体またはクラスオブジェクトへの参照なら、エラーです。
例
-
class A { int opAdd(int i); } A a; a + 1; // a.opAdd(1) と同じ 1 + a; // a.opAdd(1) と同じ
-
class B { int opDiv_r(int i); } B b; 1 / b; // b.opDiv_r(1) と同じ
class A { int opAdd(int i); } class B { int opAdd_r(A a); } A a; B b; a + 1; // a.opAdd(1) と同じ a + b; // b.opAdd_r(a) と同じ b + a; // b.opAdd_r(a) と同じ
class A { int opAdd(B b); int opAdd_r(B b); } class B { } A a; B b; a + b; // a.opAdd(b) と同じ b + a; // a.opAdd_r(b) と同じ
class A { int opAdd(B b); int opAdd_r(B b); } class B { int opAdd_r(A a); } A a; B b; a + b; // 曖昧: a.opAdd(b) or b.opAdd_r(a) b + a; // a.opAdd_r(b) と同じ
== と != のオーバーロード
どちらの演算子も関数 opEquals() を使います。 (a == b) は a.opEquals(b) と書き直され、 (a != b) は !a.opEquals(b) と書き直されます。
メンバ関数 opEquals() がObjectクラスの一部として次のように定義されています:
int opEquals(Object o);
つまり、全てのクラスオブジェクトが opEquals() を持っています。 しかし、== や != を使う可能性のあるクラスでは全て、 opEquals をオーバーライドしておく必要があると考えるべきでしょう。 オーバーライドするときの引数型はそのクラスではなく、Object にします。
構造体や共用体 (以下ではまとめて構造体と呼びます) には、以下の形のメンバ関数を定義できます:
int opEquals(S s)
or:
int opEquals(S* s)
S が等値性を定義したい構造体の名前です。
構造体に opEquals 関数が定義されていない場合は、 二つの構造体の内容のビット毎の比較によって、 等値性/非等値性が決定されます。
注意: クラスオブジェクトへの参照と null を比較するには次のように書きます。
if (a is null)
下のようには書きません:
if (a == null)
後者は次のように変換され:
if (a.opEquals(null))
opEquals()が仮想関数であった場合失敗します。
<, <=, >, >= のオーバーロード
比較演算子は全て opCmp() 関数を用います。式 (a op b) は (a.opCmp(b) op 0) と書き換えられます。可換な演算は (0 op b.opCmp(a)) と書くこともできます。
メンバ関数 opCmp() がObjectクラスの一部として次のように定義されています:
int opCmp(Object o);
つまり、全てのクラスオブジェクトが opCmp() を持っています。
構造体の opCmp は、 構造体の opEquals と似たように動作します:
struct Pair { int a, b; int opCmp(Pair rhs) { if (a!=rhs.a) return a-rhs.a; return b-rhs.b; } }
構造体については、opCmp() 関数が定義されていなければ、 比較しようとするとエラーになります。
論拠
opEquals と opCmp が両方存在する理由は:
- 同値性のチェックはしばしば、 大小関係のチェックより遙かに効率よく実装できる。
- opCmp を Object に定義しておくことで、 連想配列がクラス全般で使えるようになる。
- オブジェクトによっては、大小関係という概念が意味をなさない。 Object.opCmp がエラー例外を投げるのはこれが理由です。 opCmp は、 比較に意味のあるクラスそれぞれでオーバーライドして定義します。
クラス定義での opEquals と opCmp の引数は、 Object.opEquals と Object.opCmp を適切にオーバーライドするために、 そのクラス自身の型ではなく Object 型とする必要があります。
関数呼び出し演算子のオーバーロード f()
関数呼び出し演算子、()、は opCall という名前の関数を宣言することでオーバーロードできます:
struct F { int opCall(); int opCall(int x, int y, int z); } void test() { F f; int i; i = f(); // i = f.opCall(); と同じ i = f(3,4,5); // i = f.opCall(3,4,5); と同じ }
このようにして、構造体やクラスオブジェクトを、 あたかも関数であるかのように振る舞わせることができます。
配列演算子のオーバーロード
添字のオーバーロード a[i]
配列の添字演算子、[]、は opIndex という名前で 1個以上の引数を取る関数を宣言することでオーバーロードできます。 配列への代入は、 2個以上の引数を取る opIndexAssign 関数で行います。 第一引数が代入の右辺値です。
struct A { int opIndex(size_t i1, size_t i2, size_t i3); int opIndexAssign(int value, size_t i1, size_t i2); } void test() { A a; int i; i = a[5,6,7]; // i = a.opIndex(5,6,7); と同じ a[i,3] = 7; // a.opIndexAssign(7,i,3); と同じ }
このようにして、構造体やクラスオブジェクトを、 あたかも配列であるかのように振る舞わせることができます。
注意: 配列添字のオーバーロードは、現在のところ、 左辺値としての op=, ++, -- などの演算子に対応していません。
スライスのオーバーロード a[] and a[i .. j]
スライス演算子のオーバーロードとは、 a[] や a[i .. j] といった式に別の意味を持たせることです。 これは、opSlice という名前の関数を宣言することで実現します。 スライスへの代入は opSliceAssign を宣言することで実現します。
class A { int opSlice(); // a[] をオーバーロード int opSlice(size_t x, size_t y); // a[i .. j] をオーバーロード int opSliceAssign(int v); // a[] = v をオーバーロード int opSliceAssign(int v, size_t x, size_t y); // a[i .. j] = v をオーバーロード } void test() { A a = new A(); int i; int v; i = a[]; // i = a.opSlice(); と同じ i = a[3..4]; // i = a.opSlice(3,4); と同じ a[] = v; // a.opSliceAssign(v); と同じ a[3..4] = v; // a.opSliceAssign(v,3,4); と同じ }
代入演算子のオーバーロード
代入演算子 = は、左辺値が構造体かクラスで、 opAssign がそのメンバ関数であるときにオーバーロードされます。
左辺値に暗黙変換されるような右辺値に対しては、 代入演算子をオーバーロードすることはできません。 さらに、opAssign 関数の型として以下のようなものは使えないことがあります:
opAssign(...) opAssign(T) opAssign(T, ...) opAssign(T ...) opAssign(T, U = defaultValue, etc.)
禁止されるのは、型 T が左辺値の型 A と同じであるか、 A に暗黙変換できるか、A が構造体で T が A へ暗黙変換できるような型へのポインタ型である場合です。
今後の方向性
演算子 ! && || ?: と、 他いくつかの演算子のオーバーロードには対応する予定はありません。