演算子オーバーロード
演算子オーバーロードは、クラスや構造体をオペランドとして持つ演算子式を、 特別な名前のメンバ関数呼び出しに変換することで実現されています。 新しい文法の追加はありません。
単項演算子のオーバーロード
演算子 | 書き換え結果 |
---|---|
-e | e.opUnary!("-")() |
+e | e.opUnary!("+")() |
~e | e.opUnary!("~")() |
*e | e.opUnary!("*")() |
++e | e.opUnary!("++")() |
--e | e.opUnary!("--")() |
例えば、- (符号反転) 演算子を構造体 S でオーバーロードし、 他の演算子はオーバーロードしない場合は、こうなります:
struct S {
int m;
int opUnary(string s)() if (s == "-") {
return -m;
}
}
int foo(S s) {
return -s;
}
後置インクリメント e++ と 後置デクリメント e-- 演算子
これらの演算子は直接オーバーロードはできません。 代わりに、前置演算の ++e や --e を使った書き換えが行われます:
演算子 | 書き換え結果 |
---|---|
e-- | (auto t = e, --e, t) |
e++ | (auto t = e, ++e, t) |
単項インデックス演算子
演算子 | 書き換え結果 |
---|---|
-a[b1, b2, ... bn] | a.opIndexUnary!("-")(b1, b2, ... bn) |
+a[b1, b2, ... bn] | a.opIndexUnary!("+")(b1, b2, ... bn) |
~a[b1, b2, ... bn] | a.opIndexUnary!("~")(b1, b2, ... bn) |
*a[b1, b2, ... bn] | a.opIndexUnary!("*")(b1, b2, ... bn) |
++a[b1, b2, ... bn] | a.opIndexUnary!("++")(b1, b2, ... bn) |
--a[b1, b2, ... bn] | a.opIndexUnary!("--")(b1, b2, ... bn) |
単項スライス演算子
演算子 | 書き換え結果 |
---|---|
-a[i..j] | a.opSliceUnary!("-")(i, j) |
+a[i..j] | a.opSliceUnary!("+")(i, j) |
~a[i..j] | a.opSliceUnary!("~")(i, j) |
*a[i..j] | a.opSliceUnary!("*")(i, j) |
++a[i..j] | a.opSliceUnary!("++")(i, j) |
--a[i..j] | a.opSliceUnary!("--")(i, j) |
-a[ ] | a.opSliceUnary!("-")() |
+a[ ] | a.opSliceUnary!("+")() |
~a[ ] | a.opSliceUnary!("~")() |
*a[ ] | a.opSliceUnary!("*")() |
++a[ ] | a.opSliceUnary!("++")() |
--a[ ] | a.opSliceUnary!("--")() |
キャスト演算子のオーバーロード
演算子 | 書き換え結果 |
---|---|
cast(type)e | e.opCast!(type)() |
bool 演算
単項演算子のオーバーロードの表に欠けているものとして、特に、 論理否定の ! 演算子があります。微妙なところでは、 bool 値への暗黙変換演算が欠けています。 代わりに、これらは以下のような書き換えで実行されます:
opCast!(bool)(e)
つまり
if (e) => if (e.opCast!(bool))
if (!e) => if (!e.opCast!(bool))
などのように、bool値が期待される位置の式は処理されます。 ただし、これは構造体のインスタンスのみに限ります。クラスへの参照は常に、 null かどうかに応じてboolへと変換されます。
二項演算子のオーバーロード
以下の二項演算子がオーバーロード可能です。
+ | - | * | / | % | ^^ | & |
| | ^ | << | >> | >>> | ~ | in |
式:
a op b
は、以下の二通りの書き換えが考慮され:
a.opBinary!("op")(b)
b.opBinaryRight!("op")(a)
型が「より良く」マッチをする方が選択されます。 どちらのマッチも同程度に良い場合、エラーになります。
複数の演算子に対するオーバーロードを同時に定義できます。 例えば、+ と - 演算子だけをサポートするなら:
T opBinary(string op)(T rhs) {
static if (op == "+") return data + rhs.data;
else static if (op == "-") return data - rhs.data;
else static assert(0, "Operator "~op~" not implemented");
}
全てを一度にやるならば、こうです:
T opBinary(string op)(T rhs) {
return mixin("data "~op~" rhs.data");
}
== と != のオーバーロード
a != b の形の式は、まず !(a == b) に書き換えられます。
a == b に対しては:
- a と b の両方がクラスオブジェクトなら、式は:
.object.opEquals(a, b)
このように書き換えられ、この関数は次のように実装されています:
bool opEquals(Object a, Object b) { if (a is b) return true; if (a is null || b is null) return false; if (typeid(a) == typeid(b)) return a.opEquals(b); return a.opEquals(b) && b.opEquals(a); }
- それ以外の場合、a.opEquals(b) と b.opEquals(a) が試みられます。どちらも同じ opEquals 関数へと解決される場合は a.opEquals(b) が優先されます。
- 片方がもう一方より良いマッチである場合、 あるいは片方のみがコンパイルできる場合、そちらが選択されます。
- それ以外の場合は、エラーとなります。
クラスに対して Object.opEquals() をオーバーライドする場合、 以下のシグネチャとします:
class C {
override bool opEquals(Object o) { ... }
}
構造体のメンバ関数として opEquals を定義する場合、 以下の形式とします:
struct S {
int opEquals(ref const S s) { ... }
}
<, <=, >, >= のオーバーロード
比較演算子は以下のように書き換えられます:
比較 | 書き換え候補1 | 書き換え候補2 |
---|---|---|
a < b | a.opCmp(b) < 0 | b.opCmp(a) > 0 |
a <= b | a.opCmp(b) <= 0 | b.opCmp(a) >= 0 |
a > b | a.opCmp(b) > 0 | b.opCmp(a) < 0 |
a >= b | a.opCmp(b) >= 0 | b.opCmp(a) <= 0 |
両方の書き換えを試し、片方のみがコンパイル可能なら、そちらを取ります。 双方とも同じ関数に解決されるならば、 1つめの書き換えを優先します。異なる関数になる場合は、 良いマッチとなる方を選びます。どちらのマッチも同じくらい良い場合は、 あいまいなためエラーとなります。
クラスに対して Object.opCmp() をオーバーライドする場合、 以下のシグネチャとします:
class C {
override int opCmp(Object o) { ... }
}
構造体のメンバ関数として opCmp を定義する場合、 以下の形式とします:
struct S {
int opCmp(ref const S s) const { ... }
}
関数呼び出し演算子のオーバーロード 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); と同じ
}
このようにして、構造体やクラスオブジェクトを、 あたかも関数であるかのように振る舞わせることができます。
代入演算子のオーバーロード
= の左辺が構造体/集成体の左辺値で、 opAssign メンバ関数が定義されていれば、 オーバーロードとなります。
右辺値に対しては、 暗黙に左辺値型にキャスト可能であっても、代入演算子はオーバーロードできません。 さらに、opAssign のシグネチャとして、以下は:
opAssign(...)
opAssign(T)
opAssign(T, ...)
opAssign(T ...)
opAssign(T, U = defaultValue, etc.)
T が元の型 A と同じか A へ暗黙変換可能、 あるいは A が構造体で T が A へ暗黙変換可能な型へのポインタである場合、許されません。
インデックス代入演算子のオーバーロード
代入演算子の左辺がクラスや構造体に対するインデックス式であれば、 opIndexAssign メンバ関数を提供することで、オーバーロードできます。 a[b1, b2, ... bn] = c の形の式は a.opIndexAssign(c, b1, b2, ... bn) と書き換えられます。
struct A {
int opIndexAssign(int value, size_t i1, size_t i2);
}
void test() {
A a;
a[i,3] = 7; // a.opIndexAssign(7,i,3); と同じ
}
スライス代入演算子のオーバーロード
代入演算子の左辺がクラスや構造体に対するスライス式であれば、 opSliceAssign メンバ関数を提供することで、オーバーロードできます。 a[i..j] = c の形の式は a.opSliceAssign(c, i, j) と書き換えられ、 a[] = c は a.opSliceAssign(c) となります。
struct A {
int opSliceAssign(int v); // overloads a[] = v
int opSliceAssign(int v, size_t x, size_t y); // a[i .. j] = v をオーバーロード
}
void test() {
A a;
int v;
a[] = v; // a.opSliceAssign(v); と同じ
a[3..4] = v; // a.opSliceAssign(v,3,4); と同じ
}
演算代入演算子のオーバーロード
以下の演算代入演算子がオーバーロード可能です:
+= | -= | *= | /= | %= | ^^= | &= |
|= | ^= | <<= | >>= | >>>= | ~= |
以下の式:
a op= b
は、以下に書き換わります:
a.opOpAssign!("op")(b)
インデックス演算代入演算子のオーバーロード
op= 演算子の左辺がクラスや構造体に対するインデックス式で、 opIndexOpAssign というメンバが存在すれば:
a[b1, b2, ... bn] op= c
は、以下に書き換わります:
a.opIndexOpAssign!("op")(c, b1, b2, ... bn)
スライス演算代入演算子のオーバーロード
op= 演算子の左辺がクラスや構造体に対するスライス式で、 opSliceOpAssign というメンバが存在すれば:
a[i..j] op= c
は、以下のように書き換えられ:
a.opSliceOpAssign!("op")(c, i, j)
さらに
a[] op= c
は、以下に書き換わります:
a.opSliceOpAssign!("op")(c)
インデックス演算子のオーバーロード
配列のインデックス演算 a[b1, b2, ... bn] は opIndex という名前の1引数以上の関数を宣言することで、 オーバーロードできます。
struct A {
int opIndex(size_t i1, size_t i2, size_t i3);
}
void test() {
A a;
int i;
i = a[5,6,7]; // i = a.opIndex(5,6,7); と同じ
}
このようにして、 構造体やクラスオブジェクトを配列であるかのように振る舞わせることができます。
インデックス式が opIndexAssign や opIndexOpAssign を用いて書き換え可能な場合は、 そちらの方が優先されます。
スライス演算子のオーバーロード
スライス演算子のオーバーロードとは、 a[] や a[i..j] のような式のオーバーロードを意味します。 これは、opSlice という名前のメンバ関数を宣言することで実現します。
class A {
int opSlice(); // a[] をオーバーロード
int opSlice(size_t x, size_t y); // a[i .. j] をオーバーロード
}
void test() {
A a = new A();
int i;
int v;
i = a[]; // i = a.opSlice(); と同じ
i = a[3..4]; // i = a.opSlice(3,4); と同じ
}
スライス式が opSliceAssign や opSliceOpAssign を用いて書き換え可能な場合は、 そちらの方が優先されます。
メンバ転送
クラスや構造体のメンバとして名前が見つからなかった場合は、 opDispatch という名前のテンプレート関数が呼び出されます。
import std.stdio;
struct S {
void opDispatch(string s, T)(T i)
{
writefln("S.opDispatch('%s', %s)", s, i);
}
}
class C {
void opDispatch(string s)(int i) {
writefln("C.opDispatch('%s', %s)", s, i);
}
}
struct D {
template opDispatch(string s) {
enum int opDispatch = 8;
}
}
void main() {
S s;
s.opDispatch!("hello")(7);
s.foo(7);
auto c = new C();
c.foo(8);
D d;
writefln("d.foo = %s", d.foo);
assert(d.foo == 8);
}