D Programming Language Tutorial
Ali Çehreli



İngilizce Kaynaklar

Diğer



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:

  1. 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 to opOpAssign.
  2. 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.

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 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. */
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:

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:

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 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 };
    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 highlighted 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

... the solution