Operator Overloading
The topics covered in this chapter apply mostly for classes as well. The biggest difference is that the behavior of assignment operation opAssign()
cannot be overloaded for classes.
Operator overloading involves many concepts, some of which will be covered later in the book (templates, auto ref
, etc.). For that reason, you may find this chapter to be harder to follow than the previous ones.
Operator overloading enables defining how user-defined types behave when used with operators. In this context, the term overload means providing the definition of an operator for a specific type.
We have seen how to define structs and their member functions in previous chapters. As an example, we have defined the increment()
member function to be able to add Duration
objects to TimeOfDay
objects. Here are the two structs from previous chapters, with only the parts that are relevant to this chapter:
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)); }
A benefit of member functions is being able to define operations of a type alongside the member variables of that type.
Despite their advantages, member functions can be seen as being limited compared to operations on fundamental types. After all, fundamental types can readily be used with operators:
int weight = 50; weight += 10; // by an operator
According to what we have seen so far, similar operations can only be achieved by member functions for user-defined types:
auto lunchTime = TimeOfDay(12, 0); lunchTime.increment(Duration(10)); // by a member function
Operator overloading enables using structs and classes with operators as well. For example, assuming that the +=
operator is defined for TimeOfDay
, the operation above can be written in exactly the same way as with fundamental types:
lunchTime += Duration(10); // by an operator // (even for a struct)
Before getting to the details of operator overloading, let's first see how the line above would be enabled for TimeOfDay
. What is needed is to redefine the increment()
member function under the special name opOpAssign(string op)
and also to specify that this definition is for the +
character. As it will be explained below, this definition actually corresponds to the +=
operator.
The definition of this member function does not look like the ones that we have seen so far. That is because opOpAssign
is actually a function template. Since we will see templates in much later chapters, I will have to ask you to accept the operator overloading syntax as is for now:
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; } }
The template definition consists of two parts:
opOpAssign(string op)
: This part must be written as is and should be accepted as the name of the function. We will see below that there are other member functions in addition toopOpAssign
.if (op ==
:"+"
)opOpAssign
is used for more than one operator overload.
specifies that this is the operator overload that corresponds to the"+"
+
character. This syntax is a template constraint, which will also be covered in later chapters.
Also note that this time the return type is different from the return type of the increment()
member function: It is not void
anymore. We will discuss the return types of operators later below.
Behind the scenes, the compiler replaces the uses of the +=
operator with calls to the opOpAssign!
member function:
"+"
lunchTime += Duration(10); // The following line is the equivalent of the previous one lunchTime.opOpAssign!"+"(Duration(10));
The !
part that is after "+"
opOpAssign
specifies that this call is for the definition of the operator for the +
character. We will cover this template syntax in later chapters as well.
Note that the operator definition that corresponds to +=
is defined by "+"
, not by "+="
. The Assign
in the name of opOpAssign()
already implies that this name is for an assignment operator.
Being able to define the behaviors of operators brings a responsibility: The programmer must observe expectations. As an extreme example, the previous operator could have been defined to decrement the time value instead of incrementing it. However, people who read the code would still expect the value to be incremented by the +=
operator.
To some extent, the return types of operators can also be chosen freely. Still, general expectations must be observed for the return types as well.
Keep in mind that operators that behave unnaturally would cause confusion and bugs.
Overloadable operators
There are different kinds of operators that can be overloaded.
Unary operators
An operator that takes a single operand is called a unary operator:
++weight;
++
is a unary operator because it works on a single variable.
Unary operators are defined by member functions named opUnary
. opUnary
does not take any parameters because it uses only the object that the operator is being executed on.
The overloadable unary operators and the corresponding operator strings are the following:
Operator | Description | Operator String |
---|---|---|
-object | negative of (numeric complement of) | "-" |
+object | the same value as (or, a copy of) | "+" |
~object | bitwise negation | "~" |
*object | access to what it points to | "*" |
++object | increment | "++" |
--object | decrement | "--" |
For example, the ++
operator for Duration
can be defined like this:
struct Duration { int minute; ref Duration opUnary(string op)() if (op == "++") { ++minute; return this; } }
Note that the return type of the operator is marked as ref
here as well. This will be explained later below.
Duration
objects can now be incremented by ++
:
auto duration = Duration(20);
++duration;
The post-increment and post-decrement operators cannot be overloaded. The object++
and object--
uses are handled by the compiler automatically by saving the previous value of the object. For example, the compiler applies the equivalent of the following code for post-increment:
/* The previous value is copied by the compiler * automatically: */ Duration __previousValue__ = duration; /* The ++ operator is called: */ ++duration; /* Then __previousValue__ is used as the value of the * post-increment operation. */
Unlike some other languages, the copy inside post-increment has no cost in D if the value of the post-increment expression is not actually used. This is because the compiler replaces such post-increment expressions with their pre-increment counterparts:
/* The value of the expression is not used below. The
* only effect of the expression is incrementing 'i'. */
i++;
Because the previous value of i
is not actually used above, the compiler replaces the expression with the following one:
/* The expression that is actually used by the compiler: */
++i;
Additionally, if an opBinary
overload supports the duration += 1
usage, then opUnary
need not be overloaded for ++duration
and duration++
. Instead, the compiler uses the duration += 1
expression behind the scenes. Similarly, the duration -= 1
overload covers the uses of --duration
and duration--
as well.
Binary operators
An operator that takes two operands is called a binary operator:
totalWeight = boxWeight + chocolateWeight;
The line above has two separate binary operators: the +
operator, which adds the values of the two operands that are on its two sides, and the =
operator that assigns the value of its right-hand operand to its left-hand operand.
The rightmost column below describes the category of each operator. The ones marked as "=" assign to the left-hand side object.
Operator | Description | Function name | Function name for right-hand side | Category |
---|---|---|---|---|
+ | add | opBinary | opBinaryRight | arithmetic |
- | subtract | opBinary | opBinaryRight | arithmetic |
* | multiply | opBinary | opBinaryRight | arithmetic |
/ | divide | opBinary | opBinaryRight | arithmetic |
% | remainder of | opBinary | opBinaryRight | arithmetic |
^^ | to the power of | opBinary | opBinaryRight | arithmetic |
& | bitwise and | opBinary | opBinaryRight | bitwise |
| | bitwise or | opBinary | opBinaryRight | bitwise |
^ | bitwise xor | opBinary | opBinaryRight | bitwise |
<< | left-shift | opBinary | opBinaryRight | bitwise |
>> | right-shift | opBinary | opBinaryRight | bitwise |
>>> | unsigned right-shift | opBinary | opBinaryRight | bitwise |
~ | concatenate | opBinary | opBinaryRight | |
in | whether contained in | opBinary | opBinaryRight | |
== | whether equal to | opEquals | - | logical |
!= | whether not equal to | opEquals | - | logical |
< | whether before | opCmp | - | sorting |
<= | whether not after | opCmp | - | sorting |
> | whether after | opCmp | - | sorting |
>= | whether not before | opCmp | - | sorting |
= | assign | opAssign | - | = |
+= | increment by | opOpAssign | - | = |
-= | decrement by | opOpAssign | - | = |
*= | multiply and assign | opOpAssign | - | = |
/= | divide and assign | opOpAssign | - | = |
%= | assign the remainder of | opOpAssign | - | = |
^^= | assign the power of | opOpAssign | - | = |
&= | assign the result of & | opOpAssign | - | = |
|= | assign the result of | | opOpAssign | - | = |
^= | assign the result of ^ | opOpAssign | - | = |
<<= | assign the result of << | opOpAssign | - | = |
>>= | assign the result of >> | opOpAssign | - | = |
>>>= | assign the result of >>> | opOpAssign | - | = |
~= | append | opOpAssign | - | = |
opBinaryRight
is for when the object can appear on the right-hand side of the operator. Let's assume a binary operator that we shall call op appears in the program:
x op y
In order to determine what member function to call, the compiler considers the following two options:
// the definition for x being on the left: x.opBinary!"op"(y); // the definition for y being on the right: y.opBinaryRight!"op"(x);
The compiler picks the option that is a better match than the other.
opBinaryRight
is useful when defining arithmetic types that would normally work on both sides of an operator like e.g. int
does:
auto x = MyInt(42); x + 1; // calls opBinary!"+" 1 + x; // calls opBinaryRight!"+"
Another common use of opBinaryRight
is the in
operator. It usually makes more sense to define opBinaryRight
for the object that appears on the right-hand side of in
. We will see an example of this below.
The parameter name rhs
that appears in the following definitions is short for right-hand side. It denotes the operand that appears on the right-hand side of the operator:
x op y
For the expression above, the rhs
parameter would represent the variable y
.
Element indexing and slicing operators
The following operators enable using a type as a collection of elements:
Description | Function Name | Sample Usage |
---|---|---|
element access | opIndex | collection[i] |
assignment to element | opIndexAssign | collection[i] = 7 |
unary operation on element | opIndexUnary | ++collection[i] |
operation with assignment on element | opIndexOpAssign | collection[i] *= 2 |
number of elements | opDollar | collection[$ - 1] |
slice of all elements | opSlice | collection[] |
slice of some elements | opSlice(size_t, size_t) | collection[i..j] |
We will cover those operators later below.
The following operator functions are from the earlier versions of D. They are discouraged:
Description | Function Name | Sample Usage |
---|---|---|
unary operation on all elements | opSliceUnary (discouraged) | ++collection[] |
unary operation on some elements | opSliceUnary (discouraged) | ++collection[i..j] |
assignment to all elements | opSliceAssign (discouraged) | collection[] = 42 |
assignment to some elements | opSliceAssign (discouraged) | collection[i..j] = 7 |
operation with assignment on all elements | opSliceOpAssign (discouraged) | collection[] *= 2 |
operation with assignment on some elements | opSliceOpAssign (discouraged) | collection[i..j] *= 2 |
Other operators
The following operators can be overloaded as well:
Description | Function Name | Sample Usage |
---|---|---|
function call | opCall | object(42) |
type conversion | opCast | to!int(object) |
dispatch for non-existent function | opDispatch | object.nonExistent() |
These operators will be explained below under their own sections.
Defining more than one operator at the same time
To keep the code samples short, we have used only the ++
, +
, and +=
operators above. It is conceivable that when one operator is overloaded for a type, many others would also need to be overloaded. For example, the --
and -=
operators are also defined for the following 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() { }
The operator overloads above have code duplications. The only differences between the similar functions are highlighted. Such code duplications can be reduced and sometimes avoided altogether by string mixins. We will see the mixin
keyword in a later chapter as well. I would like to show briefly how this keyword helps with operator overloading.
mixin
inserts the specified string as source code right where the mixin
statement appears in code. The following struct is the equivalent of the one above:
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; } }
If the Duration
objects also need to be multiplied and divided by an amount, all that is needed is to add two more conditions to the template constraint:
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); }
In fact, the template constraints are optional:
ref Duration opOpAssign(string op)(int amount) /* no constraint */ { mixin ("minute " ~ op ~ "= amount;"); return this; }
Return types of operators
When overloading an operator, it is advisable to observe the return type of the same operator on fundamental types. This would help with making sense of code and reducing confusions.
None of the operators on fundamental types return void
. This fact should be obvious for some operators. For example, the result of adding two int
values as a + b
is int
:
int a = 1; int b = 2; int c = a + b; // c gets initialized by the return value // of the + operator
The return values of some other operators may not be so obvious. For example, even operators like ++i
have values:
int i = 1; writeln(++i); // prints 2
The ++
operator not only increments i
, it also produces the new value of i
. Further, the value that is produced by ++
is not just the new value of i
, rather the variable i
itself. We can see this fact by printing the address of the result of that expression:
int i = 1; writeln("The address of i : ", &i); writeln("The address of the result of ++i: ", &(++i));
The output contains identical addresses:
The address of i : 7FFF39BFEE78 The address of the result of ++i: 7FFF39BFEE78
I recommend that you observe the following guidelines when overloading operators for your own types:
- Operators that modify the object
With the exception of
opAssign
, it is recommended that the operators that modify the object return the object itself. This guideline has been observed above with theTimeOfDay.opOpAssign!
and"+"
Duration.opUnary!
."++"
The following two steps achieve returning the object itself:
- The return type is the type of the struct, marked by the
ref
keyword to mean reference. - The function is exited by
return this
to mean return this object.
The operators that modify the object are
opUnary!
,"++"
opUnary!
, and all of the"--"
opOpAssign
overloads. - The return type is the type of the struct, marked by the
- Logical operators
opEquals
that represents both==
and!=
must returnbool
. Although thein
operator normally returns the contained object, it can simply returnbool
as well. - Sort operators
opCmp
that represents<
,<=
,>
, and>=
must returnint
. - Operators that make a new object
Some operators must make and return a new object:
- Unary operators
-
,+
, and~
; and the binary operator~
. - Arithmetic operators
+
,-
,*
,/
,%
, and^^
. - Bitwise operators
&
,|
,^
,<<
,>>
, and>>>
. - As has been seen in the previous chapter,
opAssign
returns a copy of this object byreturn this
.Note: As an optimization, sometimes it makes more sense for
opAssign
to returnconst ref
for large structs. I will not apply this optimization in this book.
As an example of an operator that makes a new object, let's define the
opBinary!
overload for"+"
Duration
. This operator should add twoDuration
objects to make and return a new one:struct Duration { int minute; Duration opBinary(string op)(Duration rhs) const if (op == "+") { return Duration(minute + rhs.minute); // new object } }
That definition enables adding
Duration
objects by the+
operator:auto travelDuration = Duration(10); auto returnDuration = Duration(11); Duration totalDuration; // ... totalDuration = travelDuration + returnDuration;
The compiler replaces that expression with the following member function call on the
travelDuration
object:// the equivalent of the expression above: totalDuration = travelDuration.opBinary!"+"(returnDuration);
- Unary operators
opDollar
Since it returns the number of elements of the container, the most suitable type for
opDollar
issize_t
. However, the return type can be other types as well (e.g.int
).- Unconstrained operators
The return types of some of the operators depend entirely on the design of the user-defined type: The unary
*
,opCall
,opCast
,opDispatch
,opSlice
, and allopIndex
varieties.
opEquals()
for equality comparisons
This member function defines the behaviors of the ==
and the !=
operators.
The return type of opEquals
is bool
.
For structs, the parameter of opEquals
can be defined as in
. However, for speed efficiency opEquals
can be defined as a template that takes auto ref const
(also note the empty template parentheses below):
bool opEquals()(auto ref const TimeOfDay rhs) const { // ... }
As we have seen in the Lvalues and Rvalues chapter, auto ref
allows lvalues to be passed by reference and rvalues by copy. However, since rvalues are not copied, rather moved, the signature above is efficient for both lvalues and rvalues.
To reduce confusion, opEquals
and opCmp
must work consistently. For two objects that opEquals
returns true
, opCmp
must return zero.
Once opEquals()
is defined for equality, the compiler uses its opposite for inequality:
x == y; // the equivalent of the previous expression: x.opEquals(y); x != y; // the equivalent of the previous expression: !(x.opEquals(y));
Normally, it is not necessary to define opEquals()
for structs. The compiler generates it for structs automatically. The automatically-generated opEquals
compares all of the members individually.
Sometimes the equality of two objects must be defined differently from this automatic behavior. For example, some of the members may not be significant in this comparison, or the equality may depend on a more complex logic.
Just as an example, let's define opEquals()
in a way that disregards the minute information altogether:
struct TimeOfDay { int hour; int minute; bool opEquals(TimeOfDay rhs) const { return hour == rhs.hour; } } // ... assert(TimeOfDay(20, 10) == TimeOfDay(20, 59));
Since the equality comparison considers the values of only the hour
members, 20:10 and 20:59 end up being equal. (This is just an example; it should be clear that such an equality comparison would cause confusions.)
opCmp()
for sorting
Sort operators determine the sort orders of objects. All of the ordering operators <
, <=
, >
, and >=
are covered by the opCmp()
member function.
For structs, the parameter of opCmp
can be defined as in
. However, as with opEquals
, it is more efficient to define opCmp
as a template that takes auto ref const
:
int opCmp()(auto ref const TimeOfDay rhs) const { // ... }
To reduce confusion, opEquals
and opCmp
must work consistently. For two objects that opEquals
returns true
, opCmp
must return zero.
Let's assume that one of these four operators is used as in the following code:
if (x op y) { // ← op is one of <, <=, >, or >=
The compiler converts that expression to the following logical expression and uses the result of the new logical expression:
if (x.opCmp(y) op 0) {
Let's consider the <=
operator:
if (x <= y) {
The compiler generates the following code behind the scenes:
if (x.opCmp(y) <= 0) {
For the user-defined opCmp()
to work correctly, this member function must return a result according to the following rules:
- A negative value if the left-hand object is considered to be before the right-hand object
- A positive value if the left-hand object is considered to be after the right-hand object
- Zero if the objects are considered to have the same sort order
To be able to support those values, the return type of opCmp()
must be int
, not bool
.
The following is a way of ordering TimeOfDay
objects by first comparing the values of the hour
members, and then comparing the values of the minute
members (only if the hour
members are equal):
int opCmp(TimeOfDay rhs) const { /* Note: Subtraction is a bug here if the result can * overflow. (See the following warning in text.) */ return (hour == rhs.hour ? minute - rhs.minute : hour - rhs.hour); }
That definition returns the difference between the minute
values when the hour
members are the same, and the difference between the hour
members otherwise. The return value would be a negative value when the left-hand object comes before in chronological order, a positive value if the right-hand object is before, and zero when they represent exactly the same time of day.
Warning: Using subtraction for the implementation of opCmp
is a bug if valid values of a member can cause overflow. For example, the two objects below would be sorted incorrectly as the object with value -2
is calculated to be greater than the one with value int.max
:
struct S { int i; int opCmp(S rhs) const { return i - rhs.i; // ← BUG } } void main() { assert(S(-2) > S(int.max)); // ← wrong sort order }
On the other hand, subtraction is acceptable for TimeOfDay
because none of the valid values of the members of that struct
can cause overflow in subtraction.
You can use std.algorithm.cmp
for comparing slices (including all string types and ranges). cmp()
compares slices lexicographically and produces a negative value, zero, or positive value depending on their order. That result can directly be used as the return value of opCmp
:
import std.algorithm; struct S { string name; int opCmp(S rhs) const { return cmp(name, rhs.name); } }
Once opCmp()
is defined, this type can be used with sorting algorithms like std.algorithm.sort
as well. As sort()
works on the elements, it is the opCmp()
operator that gets called behind the scenes to determine their order. The following program constructs 10 objects with random values and sorts them with 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); }
As expected, the elements are sorted from the earliest time to the latest time:
[03:40, 04:10, 09:06, 10:03, 10:09, 11:04, 13:42, 16:40, 18:03, 21:08]
opCall()
to call objects as functions
The parentheses around the parameter list when calling functions are operators as well. We have already seen how static opCall()
makes it possible to use the name of the type as a function. static opCall()
allows creating objects with default values at run time.
Non-static opCall()
on the other hand allows using the objects of user-defined types as functions:
Foo foo;
foo();
The object foo
above is being called like a function.
As an example, let's consider a struct
that represents a linear equation. This struct
will be used for calculating the y values of the following linear equation for specific x values:
y = ax + b
The following opCall()
simply calculates and returns the value of y according to that equation:
struct LinearEquation { double a; double b; double opCall(double x) const { return a * x + b; } }
With that definition, each object of LinearEquation
represents a linear equation for specific a and b values. Such an object can be used as a function that calculates the y values:
LinearEquation equation = { 1.2, 3.4 }; // the object is being used like a function: double y = equation(5.6);
Note: Defining opCall()
for a struct
disables the compiler-generated automatic constructor. That is why the { }
syntax is used above instead of the recommended LinearEquation(1.2, 3.4)
. When the latter syntax is desired, a static opCall()
that takes two double
parameters must also be defined.
equation
above represents the y = 1.2x + 3.4 linear equation. Using that object as a function executes the opCall()
member function.
This feature can be useful to define and store the a and b values in an object once and to use that object multiple times later on. The following code uses such an object in a loop:
LinearEquation equation = { 0.01, 0.4 }; for (double x = 0.0; x <= 1.0; x += 0.125) { writefln("%f: %f", x, equation(x)); }
That object represents the y = 0.01x + 0.4 equation. It is being used for calculating the results for x values in the range from 0.0 to 1.0.
Indexing operators
opIndex
, opIndexAssign
, opIndexUnary
, opIndexOpAssign
, and opDollar
make it possible to use indexing operators on user-defined types similar to arrays as in object[index]
.
Unlike arrays, these operators support multi-dimensional indexing as well. Multiple index values are specified as a comma-separated list inside the square brackets (e.g. object[index0, index1]
). In the following examples we will use these operators only with a single dimension and cover their multi-dimensional uses in the More Templates chapter.
The deque
variable in the following examples is an object of struct DoubleEndedQueue
, which we will define below; and e
is a variable of type int
.
opIndex
is for element access. The index that is specified inside the brackets becomes the parameter of the operator function:
e = deque[3]; // the element at index 3 e = deque.opIndex(3); // the equivalent of the above
opIndexAssign
is for assigning to an element. The first parameter is the value that is being assigned and the second parameter is the index of the element:
deque[5] = 55; // assign 55 to the element at index 5 deque.opIndexAssign(55, 5); // the equivalent of the above
opIndexUnary
is similar to opUnary
. The difference is that the operation is applied to the element at the specified index:
++deque[4]; // increment the element at index 4 deque.opIndexUnary!"++"(4); // the equivalent of the above
opIndexOpAssign
is similar to opOpAssign
. The difference is that the operation is applied to an element:
deque[6] += 66; // add 66 to the element at index 6 deque.opIndexOpAssign!"+"(66, 6);// the equivalent of the above
opDollar
defines the $
character that is used during indexing and slicing. It is for returning the number of elements in the container:
e = deque[$ - 1]; // the last element e = deque[deque.opDollar() - 1]; // the equivalent of the above
Indexing operators example
Double-ended queue is a data structure that is similar to arrays but it provides efficient insertion at the head of the collection as well. (In contrast, inserting at the head of an array is a relatively slow operation as it requires moving the existing elements to a newly created array.)
One way of implementing a double-ended queue is to use two arrays in the background but to use the first one in reverse. The element that is conceptually inserted at the head of the queue is actually appended to the head array. As a result, this operation is as efficient as appending to the end.
The following struct
implements a double-ended queue that overloads the operators that we have seen in this section:
import std.stdio; import std.string; import std.conv; struct DoubleEndedQueue // Also known as Deque { private: /* The elements are represented as the chaining of the two * member slices. However, 'head' is indexed in reverse so * that the first element of the entire collection is * head[$-1], the second one is head[$-2], etc.: * * head[$-1], head[$-2], ... head[0], tail[0], ... tail[$-1] */ int[] head; // the first group of elements int[] tail; // the second group of elements /* Determines the actual slice that the specified element * resides in and returns it as a reference. */ 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; } /* Note: As we will see in the next chapter, the following * is a simpler and more efficient implementation of * toString(): */ version (none) { void toString(void delegate(const(char)[]) sink) const { import std.format; import std.range; formattedWrite( sink, "%(%s %)", chain(head.retro, tail)); } } /* Adds a new element to the head of the collection. */ void insertAtHead(int value) { head ~= value; } /* Adds a new element to the tail of the collection. * * Sample: deque ~= value */ ref DoubleEndedQueue opOpAssign(string op)(int value) if (op == "~") { tail ~= value; return this; } /* Returns the specified element. * * Sample: deque[index] */ inout(int) opIndex(size_t index) inout { return elementAt(index); } /* Applies a unary operation to the specified element. * * Sample: ++deque[index] */ int opIndexUnary(string op)(size_t index) { mixin ("return " ~ op ~ "elementAt(index);"); } /* Assigns a value to the specified element. * * Sample: deque[index] = value */ int opIndexAssign(int value, size_t index) { return elementAt(index) = value; } /* Uses the specified element and a value in a binary * operation and assigns the result back to the same * element. * * Sample: deque[index] += value */ int opIndexOpAssign(string op)(int value, size_t index) { mixin ("return elementAt(index) " ~ op ~ "= value;"); } /* Defines the $ character, which is the length of the * collection. * * Sample: 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]); // accessing an element ++deque[4]; // incrementing an element deque[5] = 55; // assigning to an element deque[6] += 66; // adding to an element (deque ~= 100) ~= 200; writeln(deque); }
According to the guidelines above, the return type of opOpAssign
is ref
so that the ~=
operator can be chained on the same collection:
(deque ~= 100) ~= 200;
As a result, both 100 and 200 get appended to the same collection:
Element at index 3: 3 9 7 5 3 2 55 68 4 6 8 100 200
Slicing operators
opSlice
allows slicing the objects of user-defined types with the []
operator.
In addition to this operator, there are also opSliceUnary
, opSliceAssign
, and opSliceOpAssign
but they are discouraged.
D supports multi-dimensional slicing. We will see a multi-dimensional example later in the More Templates chapter. Although the methods described in that chapter can be used for a single dimension as well, they do not match the indexing operators that are defined above and they involve templates which we have not covered yet. For that reason, we will see the non-templated use of opSlice
in this chapter, which works only with a single dimension. (This use of opSlice
is discouraged as well.)
opSlice
has two distinct forms:
- The square brackets can be empty as in
deque[]
to mean all elements. - The square brackets can contain a number range as in
deque[begin..end]
to mean the elements in the specified range.
The slicing operators are relatively more complex than other operators because they involve two distinct concepts: container and range. We will see these concepts in more detail in later chapters.
In single-dimensional slicing which does not use templates, opSlice
returns an object that represents a specific range of elements of the container. The object that opSlice
returns is responsible for defining the operations that are applied on that range elements. For example, behind the scenes the following expression is executed by first calling opSlice
to obtain a range object and then applying opOpAssign!
on that object:
"*"
deque[] *= 10; // multiply all elements by 10 // The equivalent of the above: { auto range = deque.opSlice(); range.opOpAssign!"*"(10); }
Accordingly, the opSlice
operators of DoubleEndedQueue
return a special Range
object so that the operations are applied to it:
import std.exception; struct DoubleEndedQueue { // ... /* Returns a range that represents all of the elements. * ('Range' struct is defined below.) * * Sample: deque[] */ inout(Range) opSlice() inout { return inout(Range)(head[], tail[]); } /* Returns a range that represents some of the elements. * * Sample: deque[begin .. end] */ inout(Range) opSlice(size_t begin, size_t end) inout { enforce(end <= opDollar()); enforce(begin <= end); /* Determine what parts of 'head' and 'tail' * correspond to the specified range: */ if (begin < head.length) { if (end < head.length) { /* The range is completely inside 'head'. */ return inout(Range)( head[$ - end .. $ - begin], []); } else { /* Some part of the range is inside 'head' and * the rest is inside 'tail'. */ return inout(Range)( head[0 .. $ - begin], tail[0 .. end - head.length]); } } else { /* The range is completely inside 'tail'. */ return inout(Range)( [], tail[begin - head.length .. end - head.length]); } } /* Represents a range of elements of the collection. This * struct is responsible for defining the opUnary, * opAssign, and opOpAssign operators. */ struct Range { int[] headRange; // elements that are in 'head' int[] tailRange; // elements that are in 'tail' /* Applies the unary operation to the elements of the * range. */ Range opUnary(string op)() { mixin (op ~ "headRange[];"); mixin (op ~ "tailRange[];"); return this; } /* Assigns the specified value to each element of the * range. */ Range opAssign(int value) { headRange[] = value; tailRange[] = value; return this; } /* Uses each element and a value in a binary operation * and assigns the result back to that element. */ 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); }
The output:
9 7 5 3 1 0 2 4 6 8 90 70 50 -1 -1 -1 -1 40 60 80
opCast
for type conversions
opCast
defines explicit type conversions. It can be overloaded separately for each target type. As you would remember from the earlier chapters, explicit type conversions are performed by the to
function and the cast
operator.
opCast
is a template as well, but it has a different format: The target type is specified by the (T : target_type)
syntax:
target_type opCast(T : target_type)() {
// ...
}
This syntax will become clear later after the templates chapter as well.
Let's change the definition of Duration
so that it now has two members: hours and minutes. The operator that converts objects of this type to double
can be defined as in the following code:
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); // (could be 'cast(double)duration' as well) writeln(d); }
The compiler replaces the type conversion call above with the following one:
double d = duration.opCast!double();
The double
conversion function above produces 2.5 for two hours and thirty minutes:
2.5
Although opCast
is for explicit type conversions, its bool
specialization is called automatically when the variable is used in a logical expression:
struct Duration { // ... bool opCast(T : bool)() const { return (hour != 0) || (minute != 0); } } // ... if (duration) { // compiles // ... } while (duration) { // compiles // ... } auto r = duration ? 1 : 2; // compiles
Still, the bool
specialization of opCast
is not for all implicit bool
conversions:
void foo(bool b) { // ... } // ... foo(duration); // ← compilation ERROR bool b = duration; // ← compilation ERROR
Error: cannot implicitly convert expression (duration) of type Duration to bool Error: function deneme.foo (bool b) is not callable using argument types (Duration)
Catch-all operator opDispatch
opDispatch
gets called whenever a missing member of an object is accessed. All attempts to access non-existent members are dispatched to this function.
The name of the missing member becomes the template parameter value of opDispatch
.
The following code demonstrates a simple definition:
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); }
There are no compiler errors for the calls to non-existent members. Instead, all of those calls are dispatched to opDispatch
. The first template parameter is the name of the member. The parameter values that are used when calling the function appear as the parameters of opDispatch
:
Foo.opDispatch - name: aNonExistentFunction, value: 42 Foo.opDispatch - name: anotherNonExistentFunction, value: 100
The name
template parameter can be used inside the function to make decisions on how the call to that specific non-existent function should be handled:
switch (name) { // ... }
Inclusion query by opBinaryRight!"in"
This operator allows defining the behavior of the in
operator for user-defined types. in
is commonly used with associative arrays to determine whether a value for a specific key exists in the array.
Different from other operators, this operator is normally overloaded for the case where the object appears on the right-hand side:
if (time in lunchBreak) {
The compiler would use opBinaryRight
behind the scenes:
// the equivalent of the above: if (lunchBreak.opBinaryRight!"in"(time)) {
There is also !in
to determine whether a value for a specific key does not exist in the array:
if (a !in b) {
!in
cannot be overloaded because the compiler uses the negative of the result of the in
operator instead:
if (!(a in b)) { // the equivalent of the above
Example of the in
operator
The following program defines a TimeSpan
type in addition to Duration
and TimeOfDay
. The in
operator that is defined for TimeSpan
determines whether a moment in time is within that time span.
To keep the code short, the following program defines only the necessary member functions.
Note how the TimeOfDay
object is used seamlessly in the for
loop. That loop is a demonstration of how useful operator overloading can be.
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 is outside of the 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"); } } }
The output:
11:30 is outside of the lunch break 11:45 is outside of the lunch break 12:00 is during the lunch break 12:15 is during the lunch break 12:30 is during the lunch break 12:45 is during the lunch break 13:00 is outside of the lunch break 13:15 is outside of the lunch break
Exercise
Define a fraction type that stores its numerator and denominator as members of type long
. Such a type may be useful because it does not lose value like float
, double
, and real
do due to their precisions. For example, although the result of multiplying a double
value of 1.0/3 by 3 is not 1.0, multiplying a Fraction
object that represents the fraction 1/3 by 3 would be exactly 1:
struct Fraction { long num; // numerator long den; // denominator /* As a convenience, the constructor uses the default * value of 1 for the denominator. */ this(long num, long den = 1) { enforce(den != 0, "The denominator cannot be zero"); this.num = num; this.den = den; /* Ensuring that the denominator is always positive * will simplify the definitions of some of the * operator functions. */ if (this.den < 0) { this.num = -this.num; this.den = -this.den; } } /* ... you define the operator overloads ... */ }
Define operators as needed for this type to make it a convenient type as close to fundamental types as possible. Ensure that the definition of the type passes all of the following unit tests. The unit tests ensure the following behaviors:
- An exception must be thrown when constructing an object with zero denominator. (This is already taken care of by the
enforce
expression above.) - Producing the negative of the value: For example, the negative of 1/3 should be -1/3 and negative of -2/5 should be 2/5.
- Incrementing and decrementing the value by
++
and--
. - Support for four arithmetic operations: Both modifying the value of an object by
+=
,-=
,*=
, and/=
; and producing the result of using two objects with the+
,-
,*
, and/
operators. (Similar to the constructor, dividing by zero should be prevented.)As a reminder, here are the formulas of arithmetic operations that involve two fractions a/b and c/d:
- Addition: a/b + c/d = (a*d + c*b)/(b*d)
- Subtraction: a/b - c/d = (a*d - c*b)/(b*d)
- Multiplication: a/b * c/d = (a*c)/(b*d)
- Division: (a/b) / (c/d) = (a*d)/(b*c)
- The actual (and necessarily lossful) value of the object can be converted to
double
. - Sort order and equality comparisons are performed by the actual values of the fractions, not by the values of the numerators and denominators. For example, the fractions 1/3 and 20/60 must be considered to be equal.
unittest { /* Must throw when denominator is zero. */ assertThrown(Fraction(42, 0)); /* Let's start with 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)); /* Must produce the equivalent value in type 'double'. * * Note that although double cannot represent every value * precisely, 1.5 is an exception. That is why this test * is being applied at this point. */ 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)); /* Must throw when dividing by zero. */ assertThrown(Fraction(42, 1) / Fraction(0)); /* The one with lower numerator is before. */ assert(Fraction(3, 5) < Fraction(4, 5)); /* The one with larger denominator is before. */ assert(Fraction(3, 9) < Fraction(3, 8)); assert(Fraction(1, 1_000) > Fraction(1, 10_000)); /* The one with lower value is before. */ assert(Fraction(10, 100) < Fraction(1, 2)); /* The one with negative value is before. */ assert(Fraction(-1, 2) < Fraction(0)); assert(Fraction(1, -2) < Fraction(0)); /* The ones with equal values must be both <= and >=. */ 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)); /* The ones with equal values must be equal. */ assert(Fraction(1, 3) == Fraction(20, 60)); /* The ones with equal values with sign must be equal. */ assert(Fraction(-1, 2) == Fraction(1, -2)); assert(Fraction(1, 2) == Fraction(-1, -2)); }