D 1.0   D 2.0
About Japanese Translation

Last update Fri Mar 13 20:06:59 2009

演算子オーバーロード

オーバーロードは、クラスや構造体の特別な名前のメンバ関数を単項/二項演算子の 実装であると解釈することで実現しています。 新しく文法の追加はありません。

単項演算子のオーバーロード

オーバーロード可能な単項演算子
演算子 関数名
-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 についても同様です。

  1. class A { int opNeg(); }
    A a;
    -a;	// a.opNeg() と同等
    
  2. 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関数名
+yesopAddopAdd_r
-noopSubopSub_r
*yesopMulopMul_r
/noopDivopDiv_r
%noopModopMod_r
&yesopAndopAnd_r
|yesopOropOr_r
^yesopXoropXor_r
<<noopShlopShl_r
>>noopShropShr_r
>>>noopUShropUShr_r
~noopCatopCat_r
==yesopEquals -
!=yesopEquals -
<yesopCmp -
<=yesopCmp -
>yesopCmp -
>=yesopCmp -
+=noopAddAssign -
-=noopSubAssign -
*=noopMulAssign -
/=noopDivAssign -
%=noopModAssign -
&=noopAndAssign -
|=noopOrAssign -
^=noopXorAssign -
<<=noopShlAssign -
>>=noopShrAssign -
>>>=noopUShrAssign -
~=noopCatAssign -
in no opIn opIn_r

オーバーロード可能な二項演算子 op と、 対応するクラスや構造体のメンバ関数名 opfunc, 及び opfunc_r、 そして次の式:

a op b
について、 次の一連の規則によってどの形に解釈されるかが決定します:
  1. この式は、存在する全ての a.opfuncb.opfunc_r 関数を使って、次の形に書き換えられます:
    a.opfunc(b)
    b.opfunc_r(a)
    
    そして、それら全てのオーバーロードと見なして、 最適なものを実際に適用します。 a.opfunc と b.opfunc_r のどちらかが存在するのにもかかわらず、 引数とマッチするものがない場合はエラーになります。
  2. 演算子が可換ならば、 次の形の適用が可能かどうかまで調べます:
    a.opfunc_r(b)
    b.opfunc(a)
    
  3. それでも適切な関数が見つからない場合、もし ab が構造体またはクラスオブジェクトへの参照なら、エラーです。

  1. class A { int opAdd(int i); }
    A a;
    a + 1;	// a.opAdd(1) と同じ
    1 + a;	// a.opAdd(1) と同じ
    
  2. class B { int opDiv_r(int i); }
    B b;
    1 / b;	// b.opDiv_r(1) と同じ
    
  3. 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) と同じ
    
  4. 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) と同じ
    
  5. 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() 関数が定義されていなければ、 比較しようとするとエラーになります。

論拠

opEqualsopCmp が両方存在する理由は:

クラス定義での opEqualsopCmp の引数は、 Object.opEqualsObject.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 が構造体で TA へ暗黙変換できるような型へのポインタ型である場合です。

今後の方向性

演算子 ! && || ?: と、 他いくつかの演算子のオーバーロードには対応する予定はありません。