Operator Overloading
Operator overloading is accomplished by rewriting operators whose operands are class or struct objects into calls to specially named member functions. No additional syntax is used.
- Unary Operator Overloading
- Cast Operator Overloading
- Binary Operator Overloading
- Overloading == and !=
- Overloading <, <=, <, and <=
- Function Call Operator Overloading
- Assignment Operator Overloading
- Op Assignment Operator Overloading
- Index Operator Overloading
- Slice Operator Overloading
- Forwarding
Unary Operator Overloading
op | rewrite |
---|---|
-e | e.opUnary!("-")() |
+e | e.opUnary!("+")() |
~e | e.opUnary!("~")() |
*e | e.opUnary!("*")() |
++e | e.opUnary!("++")() |
--e | e.opUnary!("--")() |
For example, in order to overload the - (negation) operator for struct S, and no other operator:
struct S {
int m;
int opUnary(string s)() if (s == "-") {
return -m;
}
}
int foo(S s) {
return -s;
}
Postincrement e++ and Postdecrement e-- Operators
These are not directly overloadable, but instead are rewritten in terms of the ++e and --e prefix operators:
op | rewrite |
---|---|
e-- | (auto t = e, --e, t) |
e++ | (auto t = e, ++e, t) |
Overloading Index Unary Operators
op | rewrite |
---|---|
-a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("-")(b⊂, b⊂, ... b⊂) |
+a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("+")(b⊂, b⊂, ... b⊂) |
~a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("~")(b⊂, b⊂, ... b⊂) |
*a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("*")(b⊂, b⊂, ... b⊂) |
++a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("++")(b⊂, b⊂, ... b⊂) |
--a[b⊂, b⊂, ... b⊂] | a.opIndexUnary!("--")(b⊂, b⊂, ... b⊂) |
Overloading Slice Unary Operators
op | rewrite |
---|---|
-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 Operator Overloading
op | rewrite |
---|---|
cast(type e) | e.opCast!(type)() |
Boolean Operations
Notably absent from the list of overloaded unary operators is the ! logical negation operator. More obscurely absent is a unary operator to convert to a bool result. Instead, these are covered by a rewrite to:
opCast!(bool)(e)
So,
if (e) => if (e.opCast!(bool))
if (!e) => if (!e.opCast!(bool))
etc., whenever a bool result is expected. This only happens, however, for instances of structs. Class references are converted to bool by checking to see if the class reference is null or not.
Binary Operator Overloading
The following binary operators are overloadable:
+ | - | * | / | % | ^^ | & |
| | ^ | << | >> | >>> | ~ | in |
The expression:
a op b
is rewritten as both:
a.opBinary!("op")(b)
b.opBinaryRight!("op")(a)
and the one with the ‘better’ match is selected. It is an error for both to equally match.
Operator overloading for a number of operators can be done at the same time. For example, if only the + or - operators are supported:
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");
}
To do them all en masse:
T opBinary(string op)(T rhs) {
return mixin("data "~op~" rhs.data");
}
Overloading == and !=
Expressions of the form a != b are rewritten as !(a == b).
Given a == b :
- If a and b are both class objects, then the expression is rewritten as:
.object.opEquals(a, b)
and that function is implemented as:
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); }
- Otherwise the expressions a.opEquals(b) and b.opEquals(a) are tried. If both resolve to the same opEquals function, then the expression is rewritten to be a.opEquals(b).
- If one is a better match then the other, or one compiles and the other does not, the one is selected.
- Otherwise, an error results.
If overridding Object.opEquals() for classes, the class member function signature should look like:
class C {
override bool opEquals(Object o) { ... }
}
If structs declare an opEquals member function, it should follow the following form:
struct S {
bool opEquals(ref const S s) { ... } // for l-values (e.g. variables)
bool opEquals(const S s) { ... } // for r-values (e.g. temporaries)
}
Alternatively you can declare a single templated opEquals function with an auto ref parameter:
struct S {
bool opEquals()(auto ref const S s) inout { ... } // for l-values and r-values
}
Overloading <, <=, >, and >=
Comparison operations are rewritten as follows:
comparison | rewrite 1 | rewrite 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 |
Both rewrites are tried. If only one compiles, that one is taken. If they both resolve to the same function, the first rewrite is done. If they resolve to different functions, the best matching one is used. If they both match the same, but are different functions, an ambiguity error results.
If overriding Object.opCmp() for classes, the class member function signature should look like:
class C {
override int opCmp(Object o) { ... }
}
If structs declare an opCmp member function, it should follow the following form:
struct S {
int opCmp(ref const S s) const { ... }
}
Function Call Operator Overloading f()
The function call operator, (), can be overloaded by declaring a function named opCall:
struct F {
int opCall();
int opCall(int x, int y, int z);
}
void test() {
F f;
int i;
i = f(); // same as i = f.opCall();
i = f(3,4,5); // same as i = f.opCall(3,4,5);
}
In this way a struct or class object can behave as if it were a function.
Assignment Operator Overloading
The assignment operator = can be overloaded if the lvalue is a struct aggregate, and opAssign is a member function of that aggregate.
The assignment operator cannot be overloaded for rvalues that can be implicitly cast to the lvalue type. Furthermore, the following parameter signatures for opAssign are not allowed:
opAssign(...)
opAssign(T)
opAssign(T, ...)
opAssign(T ...)
opAssign(T, U = defaultValue, etc.)
where T is the same type as the aggregate type A, is implicitly convertible to A, or if A is a struct and T is a pointer to a type that is implicitly convertible to A.
Index Assignment Operator Overloading
If the left hand side of an assignment is an index operation on a struct or class instance, it can be overloaded by providing an opIndexAssign member function. Expressions of the form a[b⊂, b⊂, ... b⊂] = c are rewritten as a.opIndexAssign(c, b⊂, b⊂, ... b⊂).
struct A {
int opIndexAssign(int value, size_t i1, size_t i2);
}
void test() {
A a;
a[i,3] = 7; // same as a.opIndexAssign(7,i,3);
}
Slice Assignment Operator Overloading
If the left hand side of an assignment is a slice operation on a struct or class instance, it can be overloaded by providing an opSliceAssign member function. Expressions of the form a[i..j] = c are rewritten as a.opSliceAssign(c, i, j), and a[] = c as a.opSliceAssign(c).
struct A {
int opSliceAssign(int v); // overloads a[] = v
int opSliceAssign(int v, size_t x, size_t y); // overloads a[i .. j] = v
}
void test() {
A a;
int v;
a[] = v; // same as a.opSliceAssign(v);
a[3..4] = v; // same as a.opSliceAssign(v,3,4);
}
Op Assignment Operator Overloading
The following op assignment operators are overloadable:
+= | -= | *= | /= | %= | ^^= | &= |
|= | ^= | <<= | >>= | >>>= | ~= |
The expression:
a op= b
is rewritten as:
a.opOpAssign!("op")(b)
Index Op Assignment Operator Overloading
If the left hand side of an op= is an index expression on a struct or class instance and opIndexOpAssign is a member:
a[b⊂, b⊂, ... b⊂] op= c
it is rewritten as:
a.opIndexOpAssign!("op")(c, b⊂, b⊂, ... b⊂)
Slice Op Assignment Operator Overloading
If the left hand side of an op= is a slice expression on a struct or class instance and opSliceOpAssign is a member:
a[i..j] op= c
it is rewritten as:
a.opSliceOpAssign!("op")(c, i, j)
and
a[] op= c
it is rewritten as:
a.opSliceOpAssign!("op")(c)
Index Operator Overloading
The array index operator, a[b⊂, b⊂, ... b⊂], can be overloaded by declaring a function named opIndex with one or more parameters.
struct A {
int opIndex(size_t i1, size_t i2, size_t i3);
}
void test() {
A a;
int i;
i = a[5,6,7]; // same as i = a.opIndex(5,6,7);
}
In this way a struct or class object can behave as if it were an array.
If an index expression can be rewritten using opIndexAssign or opIndexOpAssign, those are preferred over opIndex.
Slice Operator Overloading
Overloading the slicing operator means overloading expressions like a[] and a[i..j]. This can be done by declaring a member function named opSlice.
class A {
int opSlice(); // overloads a[]
int opSlice(size_t x, size_t y); // overloads a[i .. j]
}
void test() {
A a = new A();
int i;
int v;
i = a[]; // same as i = a.opSlice();
i = a[3..4]; // same as i = a.opSlice(3,4);
}
If a slice expression can be rewritten using opSliceAssign or opSliceOpAssign, those are preferred over opSlice.
Forwarding
Member names not found in a class or struct can be forwarded to a template function named opDispatch for resolution.
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);
}