Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

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.

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

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:

    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.

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)(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. 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);
}

In fact, the template constraints are optional:

    ref Duration opOpAssign(string op)(in 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:

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;
    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(in 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:

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(in TimeOfDay rhs) const
    {
        /* Note: Subtraction is a bug here if the result can
         * underflow. (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 underflow. 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(in 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 underflow 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(in S rhs) const
    {
        return cmp(name, rhs.name);
    }
}

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;
import std.algorithm;

struct TimeOfDay
{
    int hour;
    int minute;

    int opCmp(in 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 };
    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, 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.

In addition to this operator, there are also opSliceUnary, opSliceAssign, and opSliceOpAssign but they are discouraged.

opSlice has two distinct forms:

Empty square brackets mean all of the elements and a number range means some of the elements.

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.

Like the indexing operators, opSlice supports multi-dimensional slicing as well. We will see a multi-dimensional example in the More Templates chapter.

In single-dimensional slicing, 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. 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 of the 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 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(in 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
Exercise