演算子オーバーロード

この章で扱うトピックは、ほとんどクラスにも適用される。最大の違いは、クラスでは代入演算子opAssign()の動作をオーバーロードできないことだ。

演算子オーバーロードには多くの概念が含まれており、その一部は本書の後半で説明する(テンプレート、auto refなど)。そのため、この章は前の章よりも理解が難しいかもしれない。

演算子オーバーロードを使用すると、演算子とともに使用されたときに、ユーザー定義型がどのように動作するかを定義することができる。この文脈では、オーバーロードとは、特定の型に対する演算子の定義を提供することを意味する。

前の章では、構造体とそのメンバー関数の定義方法について説明した。例として、TimeOfDayオブジェクトにDurationオブジェクトを追加できるように、メンバー関数increment()を定義した。前の章で説明した2つの構造体を、この章に関連する部分だけ抜粋して以下に示す。

struct Duration {
    int minute;
}

struct TimeOfDay {
    int hour;
    int minute;

    void increment(Duration duration) {
        minute += duration.minute;

        hour += minute / 60;
        minute %= 60;
        hour %= 24;
    }
}

void main() {
    auto lunchTime = TimeOfDay(12, 0);
    lunchTime.increment(Duration(10));
}
D
operator_overloading.1

メンバー関数の利点は、その型のメンバー変数とともに、その型の操作を定義できることだ。

その利点にもかかわらず、メンバー関数は、基本型に対する操作に比べて制限があるといえるだろう。結局のところ、基本型は演算子で簡単に使用できる。

int weight = 50;
weight += 10;                       // 演算子による
D

これまで見てきたように、ユーザー定義型では、同様の操作はメンバー関数でしか実現できない。

auto lunchTime = TimeOfDay(12, 0);
lunchTime.increment(Duration(10));  // メンバー関数によって
D

演算子オーバーロードを使用すると、構造体やクラスも演算子として使用できるようになる。例えば、TimeOfDayに対して+=演算子が定義されている場合、上記の操作は基本型の場合とまったく同じように記述できる。

lunchTime += Duration(10);          // 演算子による
                                    // (構造体でも)
D

演算子オーバーロードの詳細に入る前に、まず、上記の行がTimeOfDayでどのように有効になるかを確認しよう。必要なのは、increment()メンバー関数を特別な名前opOpAssign(string op)で再定義し、この定義が+文字用であることを指定することだ。後で説明するように、この定義は実際には+=演算子に対応している。

このメンバー関数の定義は、これまで見てきたものとは異なる。これは、opOpAssign実際には関数テンプレートであるためだからだ。テンプレートについては、後の章で詳しく説明するので、ここでは演算子オーバーロードの構文をそのまま受け入れる。

struct TimeOfDay {
// ...
    ref TimeOfDay opOpAssign(string op)(Duration duration) // (1)
            if (op == "+") {                               // (2)

        minute += duration.minute;
        hour += minute / 60;
        minute %= 60;
        hour %= 24;

        return this;
    }
}
D

テンプレートの定義は2つの部分から成る:

  1. opOpAssign(string op): この部分はそのまま記述し、関数の名前として受け入れる必要がある。opOpAssign以外のメンバー関数があることは、後で説明する。
  2. if (op == "+"):opOpAssignは、複数の演算子オーバーロードに使用される。 "+"は、これが+文字に対応する演算子オーバーロードであることを指定する。この構文はテンプレート制約であり、後の章で説明する。

また、今回は、戻り値の型がincrement()メンバー関数の戻り値の型とは異なっていることにも注意。これは、もはやvoidではない。演算子の戻り値の型については、後で説明する。

内部では、コンパイラは+=演算子の使用を、opOpAssign!"+"関数呼び出しに置き換える。

lunchTime += Duration(10);

// 次の行は、前の行と同じ意味である
lunchTime.opOpAssign!"+"(Duration(10));
D

The !"+"opOpAssignの後の部分は、この呼び出しが+文字のオペレーターの定義であることを指定している。このテンプレート構文については、後続の章でも説明する。

+=に対応する演算子の定義は、 "+"によって定義され、 "+="によって定義されることに注意。opOpAssign()の名前にあるAssignは、この名前が代入演算子であることを既に示している。

演算子の動作を定義できるということは、責任も伴う。プログラマは期待される動作を遵守しなければならない。極端な例としては、前の演算子を、時間値を増加させるのではなく減少させるように定義することも可能だった。しかし、コードを読む人は、+=演算子によって値が増加すると期待するだろう。

ある程度、演算子の戻り値の型も自由に選択できる。ただし、戻り値の型についても、一般的な期待は守らなければならない。

不自然な動作をする演算子は混乱やバグを引き起こす可能性があることを念頭に置いてほしい。

オーバーロード可能な演算子

オーバーロードできる演算子にはさまざまな種類がある。

一項演算子

単一のオペランドを取る演算子を単項演算子と呼ぶ:

++weight;
D

++は一元演算子である。なぜなら、単一の変数に対して動作するからだ。

単項演算子は、opUnaryという名前のメンバー関数によって定義される。opUnaryは、演算子が実行されているオブジェクトのみを使用するため、引数は取らない。

オーバーロード可能な一項演算子と、それに対応する演算子文字列は次の通りだ。

演算子説明演算子文字列
-object負の値(数値の補数)"-"
+objectと同じ値(またはそのコピー)"+"
~objectビット単位の否定"~"
*object指す先へのアクセス"*"
++object加算"++"
--object減算"--"

例えば、Durationの演算子++は次のように定義できる。

struct Duration {
    int minute;

    ref Duration opUnary(string op)()
            if (op == "++") {
        ++minute;
        return this;
    }
}
D

ここで、演算子の戻り値の型もrefと指定されていることに注意しよう。これについては、後で説明する。

Durationオブジェクトは現在、++で加算できる:

auto duration = Duration(20);
++duration;
D

後増分演算子および後減算演算子はオーバーロードできない。object++およびobject--の使用は、オブジェクトの前の値を保存することでコンパイラによって自動的に処理される。例えば、コンパイラは後増分に対して、次のコードと同等の処理を適用する。

/* 以前の値はコンパイラによって
 * 自動的にコピーされる: */
Duration __previousValue__ = duration;

/* ++演算子が呼び出される: */
++duration;

/* その後、__previousValue__が
 * 増分演算の値として使用される。 */
D

他の言語とは異なり、Dでは、後増分演算子の式が実際に使用されない場合、後増分演算子内のコピーにはコストは発生しない。これは、コンパイラが、このような後増分演算子を、それに対応する前増分演算子に置き換えるためだ。

/* 式値は以下では使用されない。
 * 式の唯一の効果は'i'を加算することだ。 */
i++;
D

上記では、iの前の値は実際には使用されていないため、コンパイラは式を次の式に置き換える。

/* コンパイラが実際に使用する式: */
++i;
D

さらに、opBinaryのオーバーロードがduration += 1の使用をサポートしている場合、opUnary++durationおよびduration++のためにオーバーロードする必要はない。その代わりに、コンパイラはバックグラウンドでduration += 1式を使用する。同様に、duration -= 1のオーバーロードは、--durationおよびduration--の使用もカバーする。

二項演算子

2つのオペランドを取る演算子を二項演算子と呼ぶ:

totalWeight = boxWeight + chocolateWeight;
D

上記の行には2つの別々の二項演算子がある。2つのオペランドの値を足し合わせる+演算子と、右側のオペランドの値を左側のオペランドに代入する=演算子だ。

下の右端の列は、各演算子のカテゴリを説明している。"="でマークされたものは、左側のオブジェクトに代入する。

演算子説明関数名右辺の関数名カテゴリ
+加算opBinaryopBinaryRight算術
-減算opBinaryopBinaryRight算術
*乗算opBinaryopBinaryRight算術
/除算opBinaryopBinaryRight算術
%余りopBinaryopBinaryRight算術
^^累乗opBinaryopBinaryRight算術
&ビット単位opBinaryopBinaryRightビット単位
|ビット単位のORopBinaryopBinaryRightビット単位
^ビット単位の排他的論理和opBinaryopBinaryRightビット単位
<<左シフトopBinaryopBinaryRightビット単位
>>右シフトopBinaryopBinaryRightビット単位
>>>符号なし右シフトopBinaryopBinaryRightビット単位
~連結opBinaryopBinaryRight
in含まれているかどうかopBinaryopBinaryRight
==等しいかどうかopEquals-論理
!=等しくないかどうかopEquals-論理
<それより前かどうかopCmp-ソート
<=後かどうかopCmp-ソート
>opCmp-ソート
>=並べ替え前opCmp-ソート
=割り当てopAssign-=
+=加算opOpAssign-=
-=減算opOpAssign-=
*=乗算と割り当てopOpAssign-=
/=除算して代入opOpAssign-=
%=の余りを代入opOpAssign-=
^^=累乗の結果を代入opOpAssign-=
&=&の結果を代入opOpAssign-=
|=|の結果を代入するopOpAssign-=
^=^の結果を割り当てるopOpAssign-=
<<=結果を<<に割り当てるopOpAssign-=
>>=>>の結果を代入するopOpAssign-=
>>>=>>>の結果を代入するopOpAssign-=
~=追記opOpAssign-=

opBinaryRightは、オブジェクトが演算子の右側に現れることができる場合に使用する。プログラムにopという名前の二項演算子が現れると仮定しよう:

x op y
D

呼び出すメンバー関数を決定するために、コンパイラは次の2つの選択肢を検討する。

// xが左側にある場合の定義:
x.opBinary!"op"(y);

// yが右側にある場合の定義:
y.opBinaryRight!"op"(x);
D

コンパイラは、もう一方よりも一致するオプションを選択する。

opBinaryRightは、通常、intのように演算子の両側で機能する算術型を定義する場合に有用だ。

auto x = MyInt(42);
x + 1;    // opBinary!"+"を呼び出す
1 + x;    // opBinaryRight!"+"を呼び出す
D

opBinaryRightのもう1つの一般的な用途は、in演算子だ。通常、inの右側に現れるオブジェクトに対して、opBinaryRightを定義するほうが理にかなっている。この例を以下に示す。

以下の定義で登場するパラメータ名rhsは、右辺の略語だ。これは、演算子の右側に現れるオペランドを表している。

x op y
D

上記の式では、rhsパラメータは変数yを表す。

要素インデックスとスライシング演算子

次の演算子を使用すると、型を要素のコレクションとして使用することができる。

説明関数名使用例
要素へのアクセスopIndexcollection[i]
要素への代入opIndexAssigncollection[i] = 7
要素に対する一項演算opIndexUnary++collection[i]
要素に対する代入を伴う演算opIndexOpAssigncollection[i] *= 2
要素の数opDollarcollection[$ - 1]
すべての要素のスライスopSlicecollection[]
一部の要素のスライスopSlice(size_t, size_t)collection[i..j]

これらの演算子は後で説明する。

以下の演算子関数は、以前のバージョンのDのものだ。これらの使用は推奨されない。

説明関数名使用例
すべての要素に対する一項演算opSliceUnary (discouraged)++collection[]
一部の要素に対する一項演算opSliceUnary (discouraged)++collection[i..j]
すべての要素への代入opSliceAssign (discouraged)collection[] = 42
一部の要素への代入opSliceAssign (discouraged)collection[i..j] = 7
すべての要素に対する代入を含む演算opSliceOpAssign (discouraged)collection[] *= 2
一部の要素に対する代入を含む演算opSliceOpAssign (discouraged)collection[i..j] *= 2
その他の演算子

以下の演算子もオーバーロードすることができる。

説明関数名使用例
関数呼び出しopCallobject(42)
型変換opCastto!int(object)
存在しない関数のディスパッチopDispatchobject.nonExistent()

これらの演算子は、以下の各セクションで説明する。

複数の演算子を同時に定義する

コード例を短くするため、ここでは++++=の3つの演算子のみを使用している。ある型に対して1つの演算子がオーバーロードされる場合、他の多くの演算子もオーバーロードする必要があることが考えられる。例えば、--および-=演算子は、次のDuration

struct Duration {
    int minute;

    ref Duration opUnary(string op)()
            if (op == "++") {
        ++minute;
        return this;
    }

    ref Duration opUnary(string op)()
            if (op == "--") {
        --minute;
        return this;
    }

    ref Duration opOpAssign(string op)(int amount)
            if (op == "+") {
        minute += amount;
        return this;
    }

    ref Duration opOpAssign(string op)(int amount)
            if (op == "-") {
        minute -= amount;
        return this;
    }
}

unittest {
    auto duration = Duration(10);

    ++duration;
    assert(duration.minute == 11);

    --duration;
    assert(duration.minute == 10);

    duration += 5;
    assert(duration.minute == 15);

    duration -= 3;
    assert(duration.minute == 12);
}

void main() {
}
D
operator_overloading.2

上記の演算子オーバーロードにはコードの重複がある。類似の関数の違いのみを強調表示している。このようなコードの重複は、文字列ミックスインを使用することで削減でき、場合によっては完全に回避することもできる。mixinキーワードについては、後の章でも説明する。このキーワードが演算子オーバーロードにどのように役立つかを簡単に紹介したいと思う。

mixinは、mixin文がコード内に現れる場所に、指定した文字列をソースコードとして挿入する。次の構造体は、上記の構造体と同じ意味だ。

struct Duration {
    int minute;

    ref Duration opUnary(string op)()
            if ((op == "++") || (op == "--")) {
        mixin (op ~ "minute;");
        return this;
    }

    ref Duration opOpAssign(string op)(int amount)
            if ((op == "+") || (op == "-")) {
        mixin ("minute " ~ op ~ "= amount;");
        return this;
    }
}
D

Durationオブジェクトが量で乗算や除算される必要がある場合、テンプレート制約に2つの条件を追加するだけで済む:

struct Duration {
// ...

    ref Duration opOpAssign(string op)(int amount)
        if ((op == "+") || (op == "-") ||
            (op == "*") || (op == "/")) {
        mixin ("minute " ~ op ~ "= amount;");
        return this;
    }
}

unittest {
    auto duration = Duration(12);

    duration *= 4;
    assert(duration.minute == 48);

    duration /= 2;
    assert(duration.minute == 24);
}
D

実際、テンプレート制約はオプションだ:

ref Duration opOpAssign(string op)(int amount)
        /* 制約なし */ {
    mixin ("minute " ~ op ~ "= amount;");
    return this;
}
D
演算子の戻り値の型

演算子をオーバーロードする場合、基本型に対する同じ演算子の戻り値の型を遵守することをお勧めする。これにより、コードの意味が理解しやすくなり、混乱が減少する。

基本型に対する演算子は、voidを返すものはない。この事実は、一部の演算子では明らかだ。例えば、2つのint値をa + bとして加算した結果は、intになる。

int a = 1;
int b = 2;
int c = a + b;  // cは+演算子の戻り値によって
                // 初期化される
D

他の演算子の戻り値はそれほど明白ではない場合もある。例えば、++iのような演算子にも値がある。

int i = 1;
writeln(++i);    // 2を表示
D

++演算子は、iを加算するだけでなく、iの新しい値も生成する。さらに、++によって生成される値は、iの新しい値ではなく、変数i自体だ。この事実は、その式の結果のアドレスを出力することで確認できる。

int i = 1;
writeln("The address of i                : ", &i);
writeln("The address of the result of ++i: ", &(++i));
D

出力には同じアドレスが含まれている:

iのアドレス7FFF39BFEE78
++iの結果のアドレス7FFF39BFEE78

独自の型に対して演算子をオーバーロードする場合は、次のガイドラインに従うことをお勧めする。

opEquals()等価比較用

このメンバー関数は、==および!=演算子の動作を定義する。

opEqualsの戻り値の型はboolである。

構造体では、opEqualsのパラメータはinとして定義できる。ただし、速度効率のため、opEqualsauto ref constを引数とするテンプレートとして定義できる (以下の空のテンプレート括弧にも注意)。

bool opEquals()(auto ref const TimeOfDay rhs) const {
    // ...
}
D

l値とr値の章で見たように、auto refにより、l値は参照で、r値はコピーで渡すことができる。ただし、r値はコピーではなく移動されるため、上記のシグネチャはl値とr値の両方に効率的だ。

混乱を避けるため、opEqualsopCmpは一貫して動作する必要がある。opEqualstrueを返す2つのオブジェクトに対して、opCmpは0を返す必要がある。

opEquals()が等価性のために定義されると、コンパイラはその逆を不等価性のために使用する。

x == y;
// 前の式と同等:
x.opEquals(y);

x != y;
// 前の式と同等:
!(x.opEquals(y));
D

通常、構造体に対してopEquals()を定義する必要はない。コンパイラは、構造体に対してこれを自動的に生成する。自動的に生成されるopEqualsは、すべてのメンバーを個別に比較する。

2つのオブジェクトの等価性を、この自動動作とは異なって定義しなければならない場合がある。例えば、一部のメンバーがこの比較では重要ではない場合や、等価性がより複雑なロジックに依存する場合などだ。

例として、分単位の情報を完全に無視するopEquals()を定義しよう。

struct TimeOfDay {
    int hour;
    int minute;

    bool opEquals(TimeOfDay rhs) const {
        return hour == rhs.hour;
    }
}
// ...
    assert(TimeOfDay(20, 10) == TimeOfDay(20, 59));
D

等価比較ではhourメンバーの値のみが考慮されるため、20:10と20:59は等価とみなされる。(これは単なる例であり、このような等価比較では混乱が生じることは明らかだ。)

opCmp()並べ替えについて

ソート演算子は、オブジェクトのソート順を決定する。すべての順序演算子<<=>、および>=は、opCmp()メンバー関数でカバーされている。

構造体については、opCmpのパラメータをinと定義することができる。ただし、opEqualsと同様に、opCmpauto ref constを引数とするテンプレートとして定義するほうが効率的だ。

int opCmp()(auto ref const TimeOfDay rhs) const {
    // ...
}
D

混乱を避けるため、opEqualsopCmpは一貫して動作する必要がある。opEqualstrueを返す2つのオブジェクトに対して、opCmpは0を返す必要がある。

次のコードのように、これらの4つの演算子のいずれかが使用されていると仮定しよう。

if (x op y) {  // ← opはis、 <、<=、>、または>=
D

コンパイラは、その式を次の論理式に変換し、新しい論理式の結果を使用する。

if (x.opCmp(y) op 0) {
D

<=演算子について考えてみよう:

if (x <= y) {
D

コンパイラは、次のようにコードを生成する:

if (x.opCmp(y) <= 0) {
D

ユーザー定義のopCmp()が正しく動作するには、このメンバー関数は次の規則に従って結果を返さなければならない。

これらの値をサポートするには、opCmp()の戻り値の型は、boolではなく、intである必要がある。

以下は、hourメンバーの値を最初に比較し、次にminuteメンバーの値を比較して (hourメンバーが等しい場合のみ)、TimeOfDayオブジェクトを順序付ける方法である。

int opCmp(TimeOfDay rhs) const {
    /* 注釈: 結果がオーバーフローする場合、ここでは減算はバグになる。
     * (本文の以下の警告を参照のこと。) */

    return (hour == rhs.hour
            ? minute - rhs.minute
            : hour - rhs.hour);
}
D

この定義は、hourメンバーが等しい場合はminute値の差を、そうでない場合はhourメンバーの差を返す。戻り値は、左側のオブジェクトが時系列で先の場合に負の値右側のオブジェクトが先の場合に正の値、両者がまったく同じ時刻を表す場合は0になる。

警告:メンバーの有効な値がオーバーフローを引き起こす可能性がある場合、opCmpの実装に減算を使用することはバグである。例えば、以下の2つのオブジェクトは、値-2のオブジェクトが値int.maxのオブジェクトよりも大きいと計算されるため、正しくソートされない。

struct S {
    int i;

    int opCmp(S rhs) const {
        return i - rhs.i;          // ← バグ
    }
}

void main() {
    assert(S(-2) > S(int.max));    // ← ソート順が間違っている
}
D
operator_overloading.3

一方、TimeOfDayでは、そのstructのメンバーの有効な値はいずれも減算でオーバーフローを起こさないため、減算を使用しても問題はない。

std.algorithm.cmpは、スライス(すべての文字列型および範囲を含む)の比較に使用できる。cmp()は、スライスを辞書順で比較し、その順序に応じて負の値、0、または正の値を生成する。この結果は、opCmpの戻り値として直接使用できる。

import std.algorithm;

struct S {
    string name;

    int opCmp(S rhs) const {
        return cmp(name, rhs.name);
    }
}
D

opCmp()が定義されると、この型はstd.algorithm.sortなどのソートアルゴリズムでも使用できるようになる。sort()は要素に対して動作するため、その順序を決定するために、バックグラウンドでopCmp()演算子が呼び出される。次のプログラムは、ランダムな値を持つ10個のオブジェクトを作成し、sort()でそれらをソートする。

import std.random;
import std.stdio;
import std.string;
import std.algorithm;

struct TimeOfDay {
    int hour;
    int minute;

    int opCmp(TimeOfDay rhs) const {
        return (hour == rhs.hour
                ? minute - rhs.minute
                : hour - rhs.hour);
    }

    string toString() const {
        return format("%02s:%02s", hour, minute);
    }
}

void main() {
    TimeOfDay[] times;

    foreach (i; 0 .. 10) {
        times ~= TimeOfDay(uniform(0, 24), uniform(0, 60));
    }

    sort(times);

    writeln(times);
}
D
operator_overloading.4

予想通り、要素は最も早い時間から最も遅い時間順に並べ替えられている:

[03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]
opCall()オブジェクトを関数として呼び出す

関数を呼び出すときのパラメータリストを囲む括弧も演算子だ。static opCall()によって、型の名前を関数として使用できることはすでに説明した。static opCall()を使用すると、実行時にデフォルト値を持つオブジェクトを作成できる。

一方、非静的opCall()を使用すると、ユーザー定義型のオブジェクトを関数として使用することができる。

Foo foo;
foo();
D

上記のオブジェクトfooは、関数のように呼び出されている。

例として、線形方程式を表すstructを考えてみよう。このstructは、特定のx値について、次の線形方程式のy値を計算するために使用される。

y = ax + b

次のopCall()は、その方程式に従ってyの値を計算して返すだけだ。

struct LinearEquation {
    double a;
    double b;

    double opCall(double x) const {
        return a * x + b;
    }
}
D

この定義により、LinearEquationの各オブジェクトは、特定のaおよびbの値に対する線形方程式を表す。このようなオブジェクトは、yの値を計算する関数として使用できる。

LinearEquation equation = { 1.2, 3.4 };
// オブジェクトは関数のように使用されている:
double y = equation(5.6);
D

注釈: structopCall()を定義すると、コンパイラによって生成される自動コンストラクタが無効になる。そのため、上記では、推奨されるLinearEquation(1.2, 3.4)ではなく、{ }という構文を使用している。後者の構文を使用する場合は、2つのdoubleパラメータを取るstatic opCall()も定義する必要がある。

equationは、y = 1.2x + 3.4という線形方程式を表している。このオブジェクトを関数として使用すると、opCall()メンバー関数が実行される。

この機能は、aおよびbの値を1回だけオブジェクトで定義して保存し、そのオブジェクトを後で複数回使用する場合に便利だ。次のコードは、このようなオブジェクトをループで使用している。

LinearEquation equation = { 0.01, 0.4 };

for (double x = 0.0; x <= 1.0; x += 0.125) {
    writefln("%f: %f", x, equation(x));
}
D

このオブジェクトは、y=0.01x+0.4の式を表している。これは、0.0から1.0までの範囲のx値の結果を計算するために使用されている。

インデックス演算子

opIndexopIndexAssignopIndexUnaryopIndexOpAssign、およびopDollarを使用すると、object[index]のように、配列と同様にユーザー定義型にインデックス演算子を使用することができる。

配列とは異なり、これらの演算子は多次元インデックスもサポートしている。複数のインデックス値は、角括弧で囲んだカンマ区切りのリストとして指定する(例:object[index0, index1])。以下の例では、これらの演算子を1次元の場合にのみ使用し、多次元での使用法はテンプレートの詳細の章で説明する。

以下の例で、deque変数は、以下で定義するstruct DoubleEndedQueueのオブジェクトであり、eint型の変数だ。

opIndexは要素へのアクセス用である。括弧内に指定したインデックスが演算子関数のパラメータになる。

e = deque[3];                    // インデックス3の要素
e = deque.opIndex(3);            // 上記と同等
D

opIndexAssignは要素への代入用だ。最初のパラメータは代入される値で、2番目のパラメータは要素のインデックスだ。

deque[5] = 55;                   // インデックス5の要素に55を代入する
deque.opIndexAssign(55, 5);      // 上記と同等
D

opIndexUnary opUnaryと類似している。違いは、指定されたインデックスの要素に操作が適用される点だ:

++deque[4];                      // インデックス4の要素を加算する
deque.opIndexUnary!"++"(4);      // 上記と同等
D

opIndexOpAssign opOpAssignと同様だ。違いは、操作が要素に適用される点である:

deque[6] += 66;                  // インデックス6の要素に66を追加する
deque.opIndexOpAssign!"+"(66, 6);// 上記と同等
D

opDollar インデックス付けとスライシング時に使用される$文字を定義する。コンテナ内の要素数を返すためのものだ:

e = deque[$ - 1];                // 最後の要素
e = deque[deque.opDollar() - 1]; // 上記と同等
D
インデックス演算子の例

両端キューは配列に似たデータ構造だが、コレクションの先頭に効率的に挿入することもできる。(対照的に、配列の先頭に挿入する操作は、既存の要素を新しく作成した配列に移動する必要があるため、比較的遅い操作である。)

ダブルエンドキューを実装する1つの方法は、背景に2つの配列を使用し、最初の配列を逆順で使用することだ。キューの先頭に概念的に挿入される要素は、実際には先頭配列の末尾に追加される。その結果、この操作は末尾への追加と同じくらい効率的だ。

次のstructは、このセクションで見た演算子をオーバーロードする両端キューを実装している。

import std.stdio;
import std.string;
import std.conv;

struct DoubleEndedQueue // デキューとも呼ばれる
{
private:

    /* 要素は2つのメンバースライスの連鎖として表される。
     * ただし、'head'は逆順でインデックス付けされるため、
     * コレクション全体の最初の要素は
     * head[$-1]、2番目の要素はhead[$-2]となる。以下同様:
     *
     * head[$-1], head[$-2], ... head[0], tail[0], ... tail[$-1]
     */
    int[] head;    // 要素の最初のグループ
    int[] tail;    // 要素の2番目のグループ

    /* 指定された要素が実際に存在するスライスを特定し、
     * その参照を返す。 */
    ref inout(int) elementAt(size_t index) inout {
        return (index < head.length
                ? head[$ - 1 - index]
                : tail[index - head.length]);
    }

public:

    string toString() const {
        string result;

        foreach_reverse (element; head) {
            result ~= format("%s ", to!string(element));
        }

        foreach (element; tail) {
            result ~= format("%s ", to!string(element));
        }

        return result;
    }

    /* 注釈: 次の章で説明するように、以下は
     * toString()のより単純で効率的な
     * 実装である。 */
    version (none) {
        void toString(void delegate(const(char)[]) sink) const {
            import std.format;
            import std.range;

            formattedWrite(
                sink, "%(%s %)", chain(head.retro, tail));
        }
    }

    /* コレクションの先頭に新しい要素を追加する。 */
    void insertAtHead(int value) {
        head ~= value;
    }

    /* コレクションの末尾に新しい要素を追加する。
     *
     * サンプル: deque ~= value
     */
    ref DoubleEndedQueue opOpAssign(string op)(int value)
            if (op == "~") {
        tail ~= value;
        return this;
    }

    /* 指定された要素を返す。
     *
     * サンプル: deque[index]
     */
    inout(int) opIndex(size_t index) inout {
        return elementAt(index);
    }

    /* 指定された要素に一項演算を適用する。
     *
     * サンプル: ++deque[index]
     */
    int opIndexUnary(string op)(size_t index) {
        mixin ("return " ~ op ~ "elementAt(index);");
    }

    /* 指定した要素に値を代入する。
     *
     * サンプル: deque[index] = value
     */
    int opIndexAssign(int value, size_t index) {
        return elementAt(index) = value;
    }

    /* 指定した要素と値を二項演算で処理し、
     * その結果を同じ要素に
     * 代入する。
     *
     * サンプル: deque[index] += value
     */
    int opIndexOpAssign(string op)(int value, size_t index) {
        mixin ("return elementAt(index) " ~ op ~ "= value;");
    }

    /* $文字を、コレクションの
     * 長さとして定義する。
     *
     * サンプル: deque[$ - 1]
     */
    size_t opDollar() const {
        return head.length + tail.length;
    }
}

void main() {
    auto deque = DoubleEndedQueue();

    foreach (i; 0 .. 10) {
        if (i % 2) {
            deque.insertAtHead(i);

        } else {
            deque ~= i;
        }
    }

    writefln("Element at index 3: %s",
             deque[3]);    // 要素へのアクセス
    ++deque[4];            // 要素の加算
    deque[5] = 55;         // 要素への代入
    deque[6] += 66;        // 要素への追加

    (deque ~= 100) ~= 200;

    writeln(deque);
}
D

上記のガイドラインに従って、opOpAssignの戻り値の型は、~=演算子を同じコレクションで連鎖できるように、refになる。

(deque ~= 100) ~= 200;
D

その結果、100と200は同じコレクションに追加される:

インデックス3の要素3
975325568468100200
スライシング演算子

opSlice []演算子を使用して、ユーザー定義型のオブジェクトをスライスすることができる。

この演算子の他に、opSliceUnaryopSliceAssignopSliceOpAssignもあるが、これらは使用しないように。

Dは多次元スライスをサポートしている。多次元の例については、テンプレートの詳細の章で後で説明する。その章で説明する方法は 1次元にも使用できるが、上記で定義したインデックス演算子と一致せず、まだ説明していないテンプレートを使用するため、ここでは説明しない。そのため、この章では、単一次元でのみ機能するテンプレートを使用しないopSliceの使い方を説明する。(このopSliceの使い方も推奨されない。)

opSliceには2つの異なる形式がある:

スライシング演算子は、他の演算子よりも比較的複雑である。これは、コンテナ範囲という2つの異なる概念を扱うためだからだ。これらの概念については、後続の章で詳細に説明する。

テンプレートを使用しない 1次元スライシングでは、opSliceは、コンテナの特定の範囲の要素を表すオブジェクトを返す。opSliceが返すオブジェクトは、その範囲の要素に適用される操作を定義する役割を担う。例えば、裏では、まずopSliceを呼び出して範囲オブジェクトを取得し、次にそのオブジェクトに opOpAssign!"*"を適用する。

deque[] *= 10;    // すべての要素に10を掛ける

// 上記と同等:
{
    auto range = deque.opSlice();
    range.opOpAssign!"*"(10);
}
D

したがって、DoubleEndedQueueopSlice演算子は、操作が適用される特別なRangeオブジェクトを返す。

import std.exception;

struct DoubleEndedQueue {
// ...

    /* すべての要素を表す範囲を返す。
     * ('Range'構造体は以下で定義されている。)
     *
     * サンプル: deque[]
     */
    inout(Range) opSlice() inout {
        return inout(Range)(head[], tail[]);
    }

    /* 一部の要素を表す範囲を返す。
     *
     * サンプル: deque[begin .. end]
     */
    inout(Range) opSlice(size_t begin, size_t end) inout {
        enforce(end <= opDollar());
        enforce(begin <= end);

        /* 'head'と'tail'のうち、指定された範囲に
         * 対応する部分を決定する: */

        if (begin < head.length) {
            if (end < head.length) {
                /* 範囲は'head'の内部に完全に含まれている。 */
                return inout(Range)(
                    head[$ - end .. $ - begin],
                    []);

            } else {
                /* 範囲の一部は'head'の内部にあり、
                 * 残りは'tail'の内部にある。 */
                return inout(Range)(
                    head[0 .. $ - begin],
                    tail[0 .. end - head.length]);
            }

        } else {
            /* 範囲は'tail'内に完全に含まれる。 */
            return inout(Range)(
                [],
                tail[begin - head.length .. end - head.length]);
        }
    }

    /* コレクションの要素の範囲を表す。この
     * 構造体は、opUnary、opAssign、
     * およびopOpAssign演算子を定義する責任を負う。 */
    struct Range {
        int[] headRange;    // 'head'にある要素
        int[] tailRange;    // 'tail'にある要素

        /* 範囲の要素に単項演算を
         * 適用する。 */
        Range opUnary(string op)() {
            mixin (op ~ "headRange[];");
            mixin (op ~ "tailRange[];");
            return this;
        }

        /* 指定した値を、範囲内の
         * 各要素に割り当てる。 */
        Range opAssign(int value) {
            headRange[] = value;
            tailRange[] = value;
            return this;
        }

        /* 各要素と値を二項演算で使用し、
         * その結果をその要素に割り当てる。 */
        Range opOpAssign(string op)(int value) {
            mixin ("headRange[] " ~ op ~ "= value;");
            mixin ("tailRange[] " ~ op ~ "= value;");
            return this;
        }
    }
}

void main() {
    auto deque = DoubleEndedQueue();

    foreach (i; 0 .. 10) {
        if (i % 2) {
            deque.insertAtHead(i);

        } else {
            deque ~= i;
        }
    }

    writeln(deque);
    deque[] *= 10;
    deque[3 .. 7] = -1;
    writeln(deque);
}
D

出力:

9753102468
907050-1-1-1-1406080
opCast型変換

opCastは、明示的な型変換を定義する。これは、各ターゲット型に対して個別にオーバーロードすることができる。前の章で覚えているように、明示的な型変換は、to関数とcast演算子によって実行される。

opCastもテンプレートだが、形式が異なる。ターゲット型は、(T : target_type)構文で指定する。

target_type opCast(T : target_type)() {
    // ...
}
D

この構文は、テンプレートの章で後ほど説明される。

Durationの定義を変更して、hoursとminutesの2つのメンバーを持つようにしよう。この型のオブジェクトをdoubleに変換する演算子は、次のコードのように定義できる。

import std.stdio;
import std.conv;

struct Duration {
    int hour;
    int minute;

    double opCast(T : double)() const {
        return hour + (to!double(minute) / 60);
    }
}

void main() {
    auto duration = Duration(2, 30);
    double d = to!double(duration);
    // ('cast(double)duration'でもよい)

    writeln(d);
}
D
operator_overloading.5

コンパイラは、上記の型変換呼び出しを次のように置き換える。

double d = duration.opCast!double();
D

上記のdouble変換関数は、2時間30分に対して2.5を返す。

2.5

opCastは明示的な型変換用だが、変数が論理式で使用されると、そのbool特殊化が自動的に呼び出される。

struct Duration {
// ...

    bool opCast(T : bool)() const {
        return (hour != 0) || (minute != 0);
    }
}

// ...

    if (duration) {               // コンパイルできる
        // ...
    }

    while (duration) {            // コンパイルできる
        // ...
    }

    auto r = duration ? 1 : 2;    // コンパイルできる
D

ただし、opCastbool特殊化は、すべての暗黙のbool変換に対応しているわけではない:

void foo(bool b) {
    // ...
}

// ...

    foo(duration);                // ← コンパイルエラー
    bool b = duration;            // ← コンパイルエラー
D
エラー: Duration型の式(duration)をboolに暗黙的に
変換できない
エラー: 関数deneme.foo (bool b)は、引数の型(Duration)を使用して
呼び出すことができない
Undefined
キャッチオール演算子opDispatch

opDispatchは、オブジェクトの欠落しているメンバーにアクセスするたびに呼び出される。存在しないメンバーにアクセスしようとした場合は、すべてこの関数にディスパッチされる。

欠落しているメンバーの名前は、opDispatchのテンプレートパラメータの値になる。

以下のコードは、簡単な定義を示している:

import std.stdio;

struct Foo {
    void opDispatch(string name, T)(T parameter) {
        writefln("Foo.opDispatch - name: %s, value: %s",
                 name, parameter);
    }
}

void main() {
    Foo foo;
    foo.aNonExistentFunction(42);
    foo.anotherNonExistentFunction(100);
}
D
operator_overloading.6

存在しないメンバーを呼び出しても、コンパイラエラーは発生しない。代わりに、これらの呼び出しはすべてopDispatchにディスパッチされる。最初のテンプレートパラメータは、メンバーの名前である。関数を呼び出すときに使用されるパラメータ値は、opDispatchのパラメータとして表示される。

Foo.opDispatch
名前
aNonExistentFunction42
anotherNonExistentFunction100

nameテンプレートパラメータは、その関数内で、その特定の存在しない関数の呼び出しをどのように処理するかを決定するために使用できる。

   switch (name) {
       // ...
   }
D
包含クエリによるopBinaryRight!"in"

この演算子を使用すると、ユーザー定義型に対するin演算子の動作を定義することができる。inは、通常、連想配列で、特定のキーの値が配列に存在するかどうかを判断するために使用される。

他の演算子とは異なり、この演算子は通常、オブジェクトが右側に現れる場合にオーバーロードされる。

if (time in lunchBreak) {
D

コンパイラは内部でopBinaryRightを使用する:

	// 上記と同等:
    if (lunchBreak.opBinaryRight!"in"(time)) {
D

特定のキーの値が配列に存在しないかどうかを判断するための!inもある。

if (a !in b) {
D

!inはオーバーロードできない。これは、コンパイラが代わりにin演算子の結果の否定を使用するためだ。

if (!(a in b)) {    // 上記と同等
D
in演算子の例

次のプログラムでは、DurationおよびTimeOfDayに加えて、TimeSpan型を定義している。TimeSpanで定義されているin演算子は、ある時点がその時間範囲内にあるかどうかを判断する。

コードを短くするため、次のプログラムでは必要なメンバー関数のみを定義している。

TimeOfDayオブジェクトが、forループでシームレスに使用されていることに注目。このループは、演算子オーバーロードの有用性を示す例である。

import std.stdio;
import std.string;

struct Duration {
    int minute;
}

struct TimeOfDay {
    int hour;
    int minute;

    ref TimeOfDay opOpAssign(string op)(Duration duration)
            if (op == "+") {
        minute += duration.minute;

        hour += minute / 60;
        minute %= 60;
        hour %= 24;

        return this;
    }

    int opCmp(TimeOfDay rhs) const {
        return (hour == rhs.hour
                ? minute - rhs.minute
                : hour - rhs.hour);
    }

    string toString() const {
        return format("%02s:%02s", hour, minute);
    }
}

struct TimeSpan {
    TimeOfDay begin;
    TimeOfDay end;    // endはspanの外側にある

    bool opBinaryRight(string op)(TimeOfDay time) const
            if (op == "in") {
        return (time >= begin) && (time < end);
    }
}

void main() {
    auto lunchBreak = TimeSpan(TimeOfDay(12, 00),
                               TimeOfDay(13, 00));

    for (auto time = TimeOfDay(11, 30);
         time < TimeOfDay(13, 30);
         time += Duration(15)) {

        if (time in lunchBreak) {
            writeln(time, " is during the lunch break");

        } else {
            writeln(time, " is outside of the lunch break");
        }
    }
}
D
operator_overloading.7

出力:

11:30は昼休み外
11:45は昼休み外
12:00は昼休み中
12:15は昼休み中
12:30は昼休み中
12:45は昼休み中
13:00は昼休み外
13:15は昼休み外
演習

分子と分母をlong型のメンバーとして格納する分数型を定義しよう。この型は、floatdoublerealのような精度による値の損失がないため、便利だ。例えば、double型の値1.0/3に3を掛けた結果は1.0ではないが、分数1/3を表すFractionオブジェクトに3を掛けると、結果は正確に1になる。

struct Fraction {
    long num;  // 分子
    long den;  // 分母

    /* 便宜上、コンストラクタは分母にデフォルト値の
     * 1を使用する。 */
    this(long num, long den = 1) {
        enforce(den != 0, "The denominator cannot be zero");

        this.num = num;
        this.den = den;

        /* 分母が常に正であることを確認することで
         * 一部の演算子関数の定義を
         * 簡略化できる。 */
        if (this.den < 0) {
            this.num = -this.num;
            this.den = -this.den;
        }
    }

    /* ... 演算子オーバーロードを定義する ... */
}
D

この型を、できるだけ基本型に近い便利な型にするために、必要に応じて演算子を定義しよう。型の定義が、以下のユニットテストをすべて合格することを確認しよう。ユニットテストは、以下の動作を保証する。

unittest {
    /* 分母が0の場合は必ずスローする。 */
    assertThrown(Fraction(42, 0));

    /* 1/3から始めよう。 */
    auto a = Fraction(1, 3);

    /* -1/3 */
    assert(-a == Fraction(-1, 3));

    /* 1/3 + 1 == 4/3 */
    ++a;
    assert(a == Fraction(4, 3));

    /* 4/3 - 1 == 1/3 */
    --a;
    assert(a == Fraction(1, 3));

    /* 1/3 + 2/3 == 3/3 */
    a += Fraction(2, 3);
    assert(a == Fraction(1));

    /* 3/3 - 2/3 == 1/3 */
    a -= Fraction(2, 3);
    assert(a == Fraction(1, 3));

    /* 1/3 * 8 == 8/3 */
    a *= Fraction(8);
    assert(a == Fraction(8, 3));

    /* 8/3 / 16/9 == 3/2 */
    a /= Fraction(16, 9);
    assert(a == Fraction(3, 2));

    /* 型'double'の同等の値を生成しなければならない。
     *
     * doubleはすべての値を正確に表現できるわけではないが、
     * 1.5は例外であることに注意。そのため、このテストは
     * この段階で実施されている。 */
    assert(to!double(a) == 1.5);

    /* 1.5 + 2.5 == 4 */
    assert(a + Fraction(5, 2) == Fraction(4, 1));

    /* 1.5 - 0.75 == 0.75 */
    assert(a - Fraction(3, 4) == Fraction(3, 4));

    /* 1.5 * 10 == 15 */
    assert(a * Fraction(10) == Fraction(15, 1));

    /* 1.5 / 4 == 3/8 */
    assert(a / Fraction(4) == Fraction(3, 8));

    /* 0で除算すると例外をスローしなければならない。 */
    assertThrown(Fraction(42, 1) / Fraction(0));

    /* 分子の小さい方が先。 */
    assert(Fraction(3, 5) < Fraction(4, 5));

    /* 分母の大きい方が先。 */
    assert(Fraction(3, 9) < Fraction(3, 8));
    assert(Fraction(1, 1_000) > Fraction(1, 10_000));

    /* 値の小さい方が先。 */
    assert(Fraction(10, 100) < Fraction(1, 2));

    /* 負の値の方が先。 */
    assert(Fraction(-1, 2) < Fraction(0));
    assert(Fraction(1, -2) < Fraction(0));

    /* 値が等しいものは、必ず<=と>=の両方を満たす必要がある。 */
    assert(Fraction(-1, -2) <= Fraction(1, 2));
    assert(Fraction(1, 2) <= Fraction(-1, -2));
    assert(Fraction(3, 7) <= Fraction(9, 21));
    assert(Fraction(3, 7) >= Fraction(9, 21));

    /* 値が等しいものは、等しくなければならない。 */
    assert(Fraction(1, 3) == Fraction(20, 60));

    /* 符号も値も等しいものは、必ず等しくなければならない。 */
    assert(Fraction(-1, 2) == Fraction(1, -2));
    assert(Fraction(1, 2) == Fraction(-1, -2));
}
D