Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

is Expression

The is expression is not related to the is operator that we saw in The null Value and the is Operator chapter, neither syntactically nor semantically:

    a is b            // is operator, which we have seen before

    is (/* ... */)    // is expression

The is expression is evaluated at compile time. It produces an int value, either 0 or 1 depending on the expression specified in parentheses. Although the expression that it takes is not a logical expression, the is expression itself is used as a compile time logical expression. It is especially useful in static if conditionals and template constraints.

The condition that it takes is always about types, which must be written in one of several syntaxes.

is (T)

Determines whether T is valid as a type.

It is difficult to come up with examples for this use at this point. We will take advantage of it in later chapters with template parameters.

    static if (is (int)) {
        writeln("valid");

    } else {
        writeln("invalid");
    }

int above is a valid type:

valid

As another example, because void is not valid as the key type of an associative array, the else block would be enabled below:

    static if (is (string[void])) {
        writeln("valid");

    } else {
        writeln("invalid");
    }

The output:

invalid
is (T Alias)

Works in the same way as the previous syntax. Additionally, defines Alias as an alias of T:

    static if (is (int NewAlias)) {
        writeln("valid");
        NewAlias var = 42; // int and NewAlias are the same

    } else {
        writeln("invalid");
    }

Such aliases are useful especially in more complex is expressions as we will see below.

is (T : OtherT)

Determines whether T can automatically be converted to OtherT.

It is used for detecting automatic type conversions which we have seen in the Type Conversions chapter, as well as relationships like "this type is of that type", which we have seen in the Inheritance chapter.

import std.stdio;

interface Clock {
    void tellTime();
}

class AlarmClock : Clock {
    override void tellTime() {
        writeln("10:00");
    }
}

void myFunction(T)(T parameter) {
    static if (is (T : Clock)) {
        // If we are here then T can be used as a Clock
        writeln("This is a Clock; we can tell the time");
        parameter.tellTime();

    } else {
        writeln("This is not a Clock");
    }
}

void main() {
    auto var = new AlarmClock;
    myFunction(var);
    myFunction(42);
}

When the myFunction() template is instantiated for a type that can be used like a Clock, then the tellTime() member function is called on its parameter. Otherwise, the else clause gets compiled:

This is a Clock; we can tell the time  ← for AlarmClock
10:00                                  ← for AlarmClock
This is not a Clock                    ← for int
is (T Alias : OtherT)

Works in the same way as the previous syntax. Additionally, defines Alias as an alias of T.

is (T == Specifier)

Determines whether T is the same type as Specifier or whether T matches that specifier.

Whether the same type

When we change the previous example to use == instead of :, the condition would not be satisfied for AlarmClock:

    static if (is (T == Clock)) {
        writeln("This is a Clock; we can tell the time");
        parameter.tellTime();

    } else {
        writeln("This is not a Clock");
    }

Although AlarmClock is a Clock, it is not exactly the same type as Clock. For that reason, now the condition is invalid for both AlarmClock and int:

This is not a Clock
This is not a Clock
Whether matches the same specifier

When Specifier is one of the following keywords, this use of is determines whether the type matches that specifier (we will see some of these keywords in later chapters):

void myFunction(T)(T parameter) {
    static if (is (T == class)) {
        writeln("This is a class type");

    } else static if (is (T == enum)) {
        writeln("This is an enum type");

    } else static if (is (T == const)) {
        writeln("This is a const type");

    } else {
        writeln("This is some other type");
    }
}

Function templates can take advantage of such information to behave differently depending on the type that the template is instantiated with. The following code demonstrates how different blocks of the template above get compiled for different types:

    auto var = new AlarmClock;
    myFunction(var);

    // (enum WeekDays will be defined below for another example)
    myFunction(WeekDays.Monday);

    const double number = 1.2;
    myFunction(number);

    myFunction(42);

The output:

This is a class type
This is an enum type
This is a const type
This is some other type
is (T identifier == Specifier)

Works in the same way as the previous syntax. identifier is either an alias of the type; or some other information depending on Specifier:

Specifier The meaning of identifier
struct alias of the type that satisfied the condition
union alias of the type that satisfied the condition
class alias of the type that satisfied the condition
interface alias of the type that satisfied the condition
super a tuple consisting of the base classes and the interfaces
enum the actual implementation type of the enum
function a tuple consisting of the function parameters
delegate the type of the delegate
return the return type of the regular function, the delegate, or the function pointer
__parameters a tuple consisting of the parameters of the regular function, the delegate, or the function pointer
const alias of the type that satisfied the condition
immutable alias of the type that satisfied the condition
shared alias of the type that satisfied the condition

Let's first define various types before experimenting with this syntax:

struct Point {
    // ...
}

interface Clock {
    // ...
}

class AlarmClock : Clock {
    // ...
}

enum WeekDays {
    Monday, Tuesday, Wednesday, Thursday, Friday,
    Saturday, Sunday
}

char foo(double d, int i, Clock c) {
    return 'a';
}

The following function template uses different specifiers with this syntax of the is expression:

void myFunction(T)(T parameter) {
    static if (is (T LocalAlias == struct)) {
        writefln("\n--- struct ---");
        // LocalAlias is the same as T. 'parameter' is the
        // struct object that has been passed to this
        // function.

        writefln("Constructing a new %s object by copying it.",
                 LocalAlias.stringof);
        LocalAlias theCopy = parameter;
    }

    static if (is (T baseTypes == super)) {
        writeln("\n--- super ---");
        // The 'baseTypes' tuple contains all of the base
        // types of T. 'parameter' is the class variable that
        // has been passed to this function.

        writefln("class %s has %s base types.",
                 T.stringof, baseTypes.length);

        writeln("All of the bases: ", baseTypes.stringof);
        writeln("The topmost base: ", baseTypes[0].stringof);
    }

    static if (is (T ImplT == enum)) {
        writeln("\n--- enum ---");
        // 'ImplT' is the actual implementation type of this
        //  enum type. 'parameter' is the enum value that has
        //  been passed to this function.

        writefln("The implementation type of enum %s is %s",
                 T.stringof, ImplT.stringof);
    }

    static if (is (T ReturnT == return)) {
        writeln("\n--- return ---");
        // 'ReturnT' is the return type of the function
        // pointer that has been passed to this function.

        writefln("This is a function with a return type of %s:",
                 ReturnT.stringof);
        writeln("    ", T.stringof);
        write("calling it... ");

        // Note: Function pointers can be called like
        // functions
        ReturnT result = parameter(1.5, 42, new AlarmClock);
        writefln("and the result is '%s'", result);
    }
}

Let's now call that function template with various types that we have defined above:

    // Calling with a struct object
    myFunction(Point());

    // Calling with a class reference
    myFunction(new AlarmClock);

    // Calling with an enum value
    myFunction(WeekDays.Monday);

    // Calling with a function pointer
    myFunction(&foo);

The output:

--- struct ---
Constructing a new Point object by copying it.

--- super ---
class AlarmClock has 2 base types.
All of the bases: (Object, Clock)
The topmost base: Object

--- enum ---
The implementation type of enum WeekDays is int

--- return ---
This is a function with a return type of char:
    char function(double d, int i, Clock c)
calling it... and the result is 'a'
is (/* ... */ Specifier, TemplateParamList)

There are four different syntaxes of the is expression that uses a template parameter list:

These syntaxes allow for more complex cases.

identifier, Specifier, :, and == all have the same meanings as described above.

TemplateParamList is both a part of the condition that needs to be satisfied and a facility to define additional aliases if the condition is indeed satisfied. It works in the same way as template type deduction.

As a simple example, let's assume that an is expression needs to match associative arrays that have keys of type string:

    static if (is (T == Value[Key],   // (1)
                   Value,             // (2)
                   Key : string)) {   // (3)

That condition can be explained in three parts where the last two are parts of the TemplateParamList:

  1. If T matches the syntax of Value[Key]
  2. If Value is a type
  3. If Key is string (remember template specialization syntax)

Having Value[Key] as the Specifier requires that T is an associative array. Leaving Value as is means that it can be any type. Additionally, the key type of the associative array must be string. As a result, the previous is expression means "if T is an associative array where the key type is string."

The following program tests that is expression with four different types:

import std.stdio;

void myFunction(T)(T parameter) {
    writefln("\n--- Called with %s ---", T.stringof);

    static if (is (T == Value[Key],
                   Value,
                   Key : string)) {

        writeln("Yes, the condition has been satisfied.");

        writeln("The value type: ", Value.stringof);
        writeln("The key type  : ", Key.stringof);

    } else {
        writeln("No, the condition has not been satisfied.");
    }
}

void main() {
    int number;
    myFunction(number);

    int[string] intTable;
    myFunction(intTable);

    double[string] doubleTable;
    myFunction(doubleTable);

    dchar[long] dcharTable;
    myFunction(dcharTable);
}

The condition is satisfied only if the key type is string:

--- Called with int ---
No, the condition has not been satisfied.

--- Called with int[string] ---
Yes, the condition has been satisfied.
The value type: int
The key type  : string

--- Called with double[string] ---
Yes, the condition has been satisfied.
The value type: double
The key type  : string

--- Called with dchar[long] ---
No, the condition has not been satisfied.