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 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(in 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 the 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)(in 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 == "+"):opOpAssignis 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.
Do not change conventional behaviors
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 confusions and bugs.
Overloadable 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 value 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. */
Overloadable 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 |
| >>> | logical 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 | opOpAssign | - | = |
| -= | decrement | 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:
x.opBinary!"op"(y); // the definition for x being on the left y.opBinaryRight!"op"(x); // the definition for y being on the right
The compiler picks the option that is a better match than the other.
In most cases it is not necessary to define opBinaryRight, except for the in operator: It usually makes more sense to define opBinaryRight for in.
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.
Other operators that can be overloaded
In addition to the unary and binary operators above, the following operators can also be overloaded:
| Description | Function Name | Sample Usage |
|---|---|---|
| function call | opCall | object(42) |
| 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 |
| slice to all elements | opSlice | collection[] |
| slice to some elements | opSlice | collection[i, j] |
| unary operation on all elements | opSliceUnary | ++collection[] |
| unary operation on some elements | opSliceUnary | ++collection[i, j] |
| assignment to all elements | opSliceAssign | collection[] = 42 |
| assignment to some elements | opSliceAssign | collection[i, j] = 7 |
| operation with assignment on all elements | opSliceOpAssign | collection[] *= 2 |
| operation with assignment on some elements | opSliceOpAssign | collection[i, j] *= 2 |
| dispatch for non-existent function | opDispatch | object.nonExistent() |
These operators will be explained below under their own sections.
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
- The return type is the type of the struct, marked by the
refkeyword to mean reference. - The function is exited by
return thisto mean return this object. - Logical operators
- Sort operators
- Operators that make a new object
- Unary operators
-,+, and~; and the binary operator~. - Arithmetic operators
+,-,*,/,%, and^^. - Bitwise operators
&,|,^,<<,>>, and>>>. - As has been seen in the previous chapter,
opAssignreturns a copy of this object byreturn this.
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 the TimeOfDay.opOpAssign!"+" and Duration.opUnary!"++".
The following two steps achieve returning the object itself:
The operators that modify the object are opUnary!"++", opUnary!"--", and all of the opOpAssign overloads.
opEquals that represents both == and != must return bool. Although the in operator normally returns the contained object, it can simply return bool as well.
opCmp that represents <, <=, >, and >= must return int.
Some operators must make and return a new object:
Note: As an optimization, sometimes it makes more sense for opAssign to return const 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 two Duration objects to make and return a new one:
struct Duration { int minute; Duration opBinary(string op)(in 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);
The return types of some of the operators depend entirely on the design of the user-defined type: The unary operator *, opCall, opCast, opDispatch, opIndex and other indexing operators, and opSlice and other slicing operators.
opEquals() for equality comparisons
This member function defines the behaviors of the == and the != operators.
The return type of opEquals is bool.
According to language rules, the parameter of opEquals for structs must be defined as const ref.
As an important rule, 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;
x.opEquals(y); // the equivalent of the previous expression
x != y;
!(x.opEquals(y)); // the equivalent of the previous expression
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(const ref 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.
According to language rules, the parameter of opCmp for structs must be defined as const ref.
As an important rule, 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(const ref TimeOfDay rhs) const { 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.
Once opCmp() is defined, the type can be used in algorithms that compare objects of that type. The .sort property of arrays is such an algorithm. The following program constructs 10 objects by random values and sorts them by .sort. As .sort works on the elements, it is the opCmp() operator that gets called behind the scenes to determine the sort orders of the elements:
import std.random; import std.stdio; import std.string; struct TimeOfDay { int hour; int minute; int opCmp(const ref 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)); } times.sort; 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 runtime.
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 };
double y = equation(5.6); // the object is being used like a function
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(), and opIndexOpAssign() make it possible to use indexing operators for user-defined types similar to arrays as in object[index]. Different from arrays, there can be more than one index in square brackets. The exact behavior of these operators depend on the user-defined type. We will see a sample program that includes these operators below.
opIndex is for element access. The indexes within brackets become the parameters of the operator function:
grade = grades[3, 1]; // access the element at 3,1 grade = grades.opIndex(3, 1); // the equivalent of the above
opIndexUnary is similar to opUnary. Its difference is that the operation is applied to the element that corresponds to the specified index(es):
++grades[4, 0]; // increment the element at 4,0 grades.opIndexUnary!"++"(4, 0); // the equivalent of the above
opIndexAssign is for assignment. Its first parameter is the value that is to be assigned, and the rest of the parameters are the index(es):
grades[1, 1] = 95; // assign 95 to element 1,1 grades.opIndexAssign(95, 1, 1); // the equivalent of the above
opIndexOpAssign is similar to opOpAssign. Its difference is that the result of the operation is assigned back to the specified element:
grades[2, 1] += 42; // add to the element at 2,1 grades.opIndexOpAssign!"+"(42, 2, 1); // the equivalent of the above
Indexing operators example
The following struct includes examples of each of these operators. In this design, the first index specifies the student and the second index specifies the grade of that student. For example, [1, 2] specifies the grade id 2 of student id 1.
import std.stdio; struct StudentGrades { // Two grades for five students int[2][5] grades; // Returns the specified grade int opIndex(size_t studentId, size_t gradeId) const { return grades[studentId][gradeId]; } // Increments the specified grade ref int opIndexUnary(string op)(size_t studentId, size_t gradeId) if (op == "++") { return ++grades[studentId][gradeId]; } // Assigns to the specified grade of the specified student int opIndexAssign(int grade, size_t studentId, size_t gradeId) { return grades[studentId][gradeId] = grade; } // Adds to the specified grade ref int opIndexOpAssign(string op)(int amount, size_t studentId, size_t gradeId) if (op == "+") { return grades[studentId][gradeId] += amount; } } void main() { StudentGrades grades; writeln("Grade for index 3,1: ", grades[3, 1]); // access ++grades[4, 0]; // increment grades[1, 1] = 95; // assignment grades[2, 1] += 42; // add-assignment writeln(grades); }
The output:
Grade for index 3,1: 0 StudentGrades([[0, 0], [0, 95], [0, 42], [0, 0], [1, 0]])
Note that the return types of opIndexUnary and opIndexOpAssign are marked as ref according to the return type guidelines above. Additionally, the operators do not do anything special for the return values; they simply return the results of the ++ and += operators that they use internally.
Slicing operators
opSlice, opSliceUnary, opSliceAssign, and opSliceOpAssign are similar to the indexing operators. Their difference is that they allow slicing the objects of user-defined types.
All of these operators have two distinct uses: The use where the square brackets are empty and the use where the square brackets contain a number range. Similar to slices, these should be defined to mean all of the elements and some of the elements, respectively.
Slicing operators are the most complex operators because they involve the two distinct concepts container and range. We will see these concepts in more detail in later chapters. These operators will make more sense only after understanding ranges.
For now, I will present an example that consists only of stub types and stub functions. Similar to earlier examples, this example uses only the "++" and "+" characters. Normally, many other operator characters must also be defined.
/* The type of the elements contained in the container. */ struct Element { // ... } /* Similar to slices, represents the concept of "element * range". */ struct Range { // ... } /* Similar to arrays and associative arrays, brings elements * together. */ struct Container { // For the object[] syntax Range opSlice() { Range allElements; // ... must provide access to all elements ... return allElements; } // For the object[i .. j] syntax Range opSlice(size_t i, size_t j) { Range specifiedElements; // ... must provide access to the specified elements ... return specifiedElements; } // For the ++object[] syntax Range opSliceUnary(string op)() if (op == "++") { Range allElements; // ... must increment the values of all elements ... return allElements; } // For the ++object[i .. j] syntax Range opSliceUnary(string op)(size_t i, size_t j) if (op == "++") { Range specifiedElements; // ... must increment the values of the specified elements ... return specifiedElements; } // For the (object[] = value) syntax Range opSliceAssign(Element value) { Range allElements; // ... must assign to all elements ... return allElements; } // For the (object[i .. j] = value) syntax Range opSliceAssign(Element value, size_t i, size_t j) { Range specifiedElements; // ... must assign to the specified elements ... return specifiedElements; } // For the (object[] += value) syntax Range opSliceOpAssign(string op)(Element value) if (op == "+") { Range allElements; // ... must add to all elements ... return allElements; } // For the (object[i .. j] += value) syntax Range opSliceOpAssign(string op)(Element value, size_t i, size_t j) if (op == "+") { Range specifiedElements; // ... must add to the specified elements ... return specifiedElements; } // ... other operator overloads as needed ... } void main() { Container c; Range r; Element e; /* The following lines are examples of what the previous * operator overloads make possible. The equivalents that * the compiler uses behind the scenes are shown as code * comments. */ r = c[]; // c.opSlice(); r = c[1 .. 5]; // c.opSlice(1, 5); ++c[]; // c.opSliceUnary!"++"(); ++c[2 .. 9]; // c.opSliceUnary!"++"(2, 9); c[] = e; // c.opSliceAssign(e); c[4 .. 6] = e; // c.opSliceAssign(e, 4, 6); c[] += e; // c.opSliceOpAssign!"+"(e); c[5 .. 8] += e; // c.opSliceOpAssign!"+"(e, 5, 8); }
opCast for type conversions
opCast defines the 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); // can 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
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)) {
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)(in Duration duration) if (op == "+") { minute += duration.minute; hour += minute / 60; minute %= 60; hour %= 24; return this; } int opCmp(const ref 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 considered to be 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
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)(in int amount) if (op == "+") { minute += amount; return this; } ref Duration opOpAssign(string op)(in 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 hilighted by yellow. 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)(in 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)(in 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); }
Exercise
- Define a fraction type that holds its numerator and denominator as members of type
long. Such a type may be useful, because it does not lose value likefloat,double, andrealdo due to their precisions. For example, although the result of multiplying adoublevalue of 1.0/3 by 3 is not 1.0, multiplying aFractionobject 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
enforceexpression above.) - Producing the negative of the value: For example, the negative of 1⁄3 should be -1⁄3.
- 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.) - 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.
As a reminder, here are the formulas of arithmetic operations that involve two fractions a⁄b and c⁄d:
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)); }
std.exception.assertThrown that appears in the tests above is for ensuring that an expression does indeed throw an exception. It works as the equivalent of the following code:
assertThrown(Fraction(42, 1) / Fraction(0)); // ... /* The equivalent of the line above */ { auto isThrown = false; try { Fraction(42, 1) / Fraction(0); } catch (Exception exc) { isThrown = true; } assert(isThrown); }
Kitaplar
Forum
Tanıtım
İletişim
Hakları