Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

assert and enforce

In the previous two chapters we have seen how exceptions and scope statements are used toward program correctness. assert is another powerful tool to achieve the same goal by ensuring that certain assumptions that the program is based on are valid.

It may sometimes be difficult to decide whether to throw an exception or to call assert. I will use assert in all of the examples below without much justification. I will explain the differences later in the chapter.

Although not always obvious, programs are full of assumptions. For example, the following function is written under the assumption that both age parameters are greater than or equal to zero:

double averageAge(double first, double second) {
    return (first + second) / 2;
}

Although it may be invalid for the program to ever have an age value that is negative, the function would still produce an average, which may be used in the program unnoticed, resulting in the program's continuing with incorrect data.

As another example, the following function assumes that it will always be called with two commands: "sing" or "dance":

void applyCommand(string command) {
    if (command == "sing") {
        robotSing();

    } else {
        robotDance();
    }
}

Because of that assumption, the robotDance() function would be called for every command other than "sing", valid or invalid.

When such assumptions are kept only in the programmer's mind, the program may end up working incorrectly. assert statements check assumptions and terminate programs immediately when they are not valid.

Syntax

assert can be used in two ways:

    assert(logical_expression);
    assert(logical_expression, message);

The logical expression represents an assumption about the program. assert evaluates that expression to validate that assumption. If the value of the logical expression is true then the assumption is considered to be valid. Otherwise the assumption is invalid and an AssertError is thrown.

As its name suggests, this exception is inherited from Error, and as you may remember from the Exceptions chapter, exceptions that are inherited from Error must never be caught. It is important for the program to be terminated right away instead of continuing under invalid assumptions.

The two implicit assumptions of averageAge() above may be spelled out by two assert calls as in the following function:

double averageAge(double first, double second) {
    assert(first >= 0);
    assert(second >= 0);

    return (first + second) / 2;
}

void main() {
    auto result = averageAge(-1, 10);
}

Those assert checks carry the meaning "assuming that both of the ages are greater than or equal to zero". It can also be thought of as meaning "this function can work correctly only if both of the ages are greater than or equal to zero".

Each assert checks its assumption and terminates the program with an AssertError when it is not valid:

core.exception.AssertError@deneme(2): Assertion failure

The part after the @ character in the message indicates the source file and the line number of the assert check that failed. According to the output above, the assert that failed is on line 2 of file deneme.d.

The other syntax of assert allows printing a custom message when the assert check fails:

    assert(first >= 0, "Age cannot be negative.");

The output:

core.exception.AssertError@deneme.d(2): Age cannot be negative.

Sometimes it is thought to be impossible for the program to ever enter a code path. In such cases it is common to use the literal false as the logical expression to fail an assert check. For example, to indicate that applyCommand() function is never expected to be called with a command other than "sing" and "dance", and to guard against such a possibility, an assert(false) can be inserted into the impossible branch:

void applyCommand(string command) {
    if (command == "sing") {
        robotSing();

    } else if (command == "dance") {
        robotDance();

    } else {
        assert(false);
    }
}

The function is guaranteed to work with the only two commands that it knows about. (Note: An alternative choice here would be to use a final switch statement.)

static assert

Since assert checks are for correct execution of programs, they are applied when the program is actually running. There can be other checks that are about the structure of the program, which can be applied even at compile time.

static assert is the counterpart of assert that is applied at compile time. The advantage is that it does not allow even compiling a program that would have otherwise run incorrectly. A natural requirement is that it must be possible to evaluate the logical expression at compile time.

For example, assuming that the title of a menu will be printed on an output device that has limited width, the following static assert ensures that it will never be wider than that limit:

    enum dstring menuTitle = "Command Menu";
    static assert(menuTitle.length <= 16);

Note that the string is defined as enum so that its length can be evaluated at compile time.

Let's assume that a programmer changes that title to make it more descriptive:

    enum dstring menuTitle = "Directional Commands Menu";
    static assert(menuTitle.length <= 16);

The static assert check prevents compiling the program:

Error: static assert  (25u <= 16u) is false

This would remind the programmer of the limitation of the output device.

static assert is even more useful when used in templates. We will see templates in later chapters.

assert even if absolutely true

I emphasize "absolutely true" because assumptions about the program are never expected to be false anyway. A large set of program errors are caused by assumptions that are thought to be absolutely true.

For that reason, take advantage of assert checks even if they feel unnecessary. Let's look at the following function that returns the days of months in a given year:

int[] monthDays(in int year) {
    int[] days = [
        31, februaryDays(year),
        31, 30, 31, 30, 31, 31, 30, 31, 30, 31
    ];

    assert((sum(days) == 365) ||
           (sum(days) == 366));

    return days;
}

That assert check may be seen as unnecessary because the function would naturally return either 365 or 366. However, those checks are guarding against potential mistakes even in the februaryDays() function. For example, the program would be terminated if februaryDays() returned 30.

Another seemingly unnecessary check can ensure that the length of the slice would always be 12:

    assert(days.length == 12);

That way, deleting or adding elements to the slice unintentionally would also be caught. Such checks are important tools toward program correctness.

assert is also the fundamental tool that is used in unit testing and contract programming, both of which will be covered in later chapters.

No value nor side effect

We have seen that expressions produce values or make side effects. assert checks do not have values nor they should have any side effects.

The D language requires that the evaluation of the logical expression must not have any side effect. assert must remain as a passive observer of program state.

Disabling assert checks

Since assert is about program correctness, they can be seen as unnecessary once the program has been tested sufficiently. Further, since assert checks produce no values nor they have side effects, removing them from the program should not make any difference.

The compiler switch -release causes the assert checks to be ignored as if they have never been included in the program:

dmd deneme.d -release

This would allow programs run faster by not evaluating potentially slow logical expressions of the assert checks.

As an exception, the assert checks that have the literal false (or 0) as the logical expression are not disabled even when the program is compiled with ‑release. This is because assert(false) is for ensuring that a block of code is never reached, and that should be prevented even for the ‑release compilations.

enforce for throwing exceptions

Not every unexpected situation is an indication of a program error. Programs may also experience unexpected inputs and unexpected environmental state. For example, the data that is entered by the user should not be validated by an assert check because invalid data has nothing to do with the correctness of the program itself. In such cases it is appropriate to throw exceptions like we have been doing in previous programs.

std.exception.enforce is a convenient way of throwing exceptions. For example, let's assume that an exception must be thrown when a specific condition is not met:

    if (count < 3) {
        throw new Exception("Must be at least 3.");
    }

enforce() is a wrapper around the condition check and the throw statement. The following is the equivalent of the previous code:

import std.exception;
// ...
    enforce(count >= 3, "Must be at least 3.");

Note how the logical expression is negated compared to the if statement. It now spells out what is being enforced.

How to use

assert is for catching programmer errors. The conditions that assert guards against in the monthDays() function and the menuTitle variable above are all about programmer mistakes.

Sometimes it is difficult to decide whether to rely on an assert check or to throw an exception. The decision should be based on whether the unexpected situation is due to a problem with how the program has been coded.

Otherwise, the program must throw an exception when it is not possible to accomplish a task. enforce() is expressive and convenient when throwing exceptions.

Another point to consider is whether the unexpected situation can be remedied in some way. If the program can not do anything special, even by simply printing an error message about the problem with some input data, then it is appropriate to throw an exception. That way, callers of the code that threw the exception can catch it to do something special to recover from the error condition.

Exercises
  1. The following program includes a number of assert checks. Compile and run the program to discover its bugs that are revealed by those assert checks.

    The program takes a start time and a duration from the user and calculates the end time by adding the duration to the start time:

    10 hours and 8 minutes after 06:09 is 16:17.
    

    Note that this problem can be written in a much cleaner way by defining struct types. We will refer to this program in later chapters.

    import std.stdio;
    import std.string;
    import std.exception;
    
    /* Reads the time as hour and minute after printing a
     * message. */
    void readTime(in string message,
                  out int hour,
                  out int minute) {
        write(message, "? (HH:MM) ");
    
        readf(" %s:%s", &hour, &minute);
    
        enforce((hour >= 0) && (hour <= 23) &&
                (minute >= 0) && (minute <= 59),
                "Invalid time!");
    }
    
    /* Returns the time in string format. */
    string timeToString(in int hour, in int minute) {
        assert((hour >= 0) && (hour <= 23));
        assert((minute >= 0) && (minute <= 59));
    
        return format("%02s:%02s", hour, minute);
    }
    
    /* Adds duration to start time and returns the result as the
     * third pair of parameters. */
    void addDuration(in int startHour, in int startMinute,
                     in int durationHour, in int durationMinute,
                     out int resultHour, out int resultMinute) {
        resultHour = startHour + durationHour;
        resultMinute = startMinute + durationMinute;
    
        if (resultMinute > 59) {
            ++resultHour;
        }
    }
    
    void main() {
        int startHour;
        int startMinute;
        readTime("Start time", startMinute, startHour);
    
        int durationHour;
        int durationMinute;
        readTime("Duration", durationHour, durationMinute);
    
        int endHour;
        int endMinute;
        addDuration(startHour, startMinute,
                    durationHour, durationMinute,
                    endHour, endMinute);
    
        writefln("%s hours and %s minutes after %s is %s.",
                 durationHour, durationMinute,
                 timeToString(startHour, startMinute),
                 timeToString(endHour, endMinute));
    }
    

    Run the program and enter 06:09 as the start time and 1:2 as the duration. Observe that the program terminates normally.

    Note: You may notice a problem with the output. Ignore that problem for now as you will discover it by the help of assert checks soon.

  2. This time enter 06:09 and 15:2. Observe that the program is terminated by an AssertError. Go to the line of the program that is indicated in the assert message and see which one of the assert checks have failed. It may take a while to discover the cause of this particular failure.
  3. Enter 06:09 and 20:0 and observe that the same assert check still fails and fix that bug as well.
  4. Modify the program to print the times in 12-hour format with an "am" or "pm" indicator.