ICS 45C Fall 2021
Notes and Examples: Contracts and Exceptions

Includes a code example with the moniker ContractsAndExceptions


Background

When we write functions or classes in C++, there are two things we need to think about: the actual code we're writing and the assumptions that underlie that code. Some programming languages allow or even require us to explicitly state more of these assumptions in our code than others. For example, C++ requires us to specify types in a lot of places and checks them at compile time, while Python lets us leave them out and fails at run time when we don't meet our own implicit assumptions.

But the assumptions are there whether we've stated them explicitly or not, and not all of the assumptions can be stated formally, even in a language like C++ where types are specified. A function that calculates a square root can probably only be given numeric input, and perhaps that numeric input would need to be non-negative in order to get a real result. In Python, you might start writing the function this way:

def sqrt(n):
    '''Returns the square root of n, which must be a non-negative number'''

with no type information specified formally, but a docstring that makes the unstated assumptions clear. Meanwhile, in C++, we might start it this way instead:

// Returns the square root of n, which must be non-negative
double sqrt(double n)

with a slightly shorter comment, since the signature of the function makes explicit the assumption that the input and output have type double, but nonetheless we still have to document the assumption that n is non-negative, since the type system does not allow us to encode that assumption directly. (Note that it would be possible to state this assumption for an integer type, since we could use unsigned int, but there is no analogous unsigned floating-point type.)

One of the things that separates better programmers from lesser ones is that they're thinking about the things they're not writing explicitly in code, in addition to the things that they are; even better, they're documenting the unwritten assumptions when they aren't obvious (or, even better, making it impossible for users of the function to do the wrong thing, by finding ways to make the assumptions explicit instead). There's value in deciding on good names for the functions you write, but a good name is not necessarily enough; if you can't explain to yourself what kinds of input a function can take, what kinds of outputs it gives for various inputs, and in what ways it might fail, you haven't thought the function's design all the way through.

Below are details about what kinds of things you ought to be thinking about when you design a function or a class, focusing at least partly on what to do about the fact that functions can fail.


Contracts

To formalize what it means to "think a function's design all the way through," we can say that functions (including member functions of classes) have a contract associated with them. Part of that contract is specified formally as the function's signature; in C++, that signature lists the name of the function, the names and types of its parameters, and the type of its result. But a contract is more than a signature; to understand a function's contract, you have to finish your thought about its design. A function's contract consists, additionally, of (at least) the following:

Classes, too, have a contract associated with them. As with functions' contracts, the contract of a class is partly made up of what's written in the class declaration, as well as the contract associated with each member function. But, additionally, there is one other thing that is included:

The ArrayList class that I wrote in the Well-Behaved Classes example has a couple of invariants, beyond just the types specified in the declaration:

Note that there's one other thing we could say: An ArrayList's size and capacity must always be non-negative. But since we declared them as unsigned ints, this is clear already.

A proposed syntax for implementing contracts

Sadly, while we can reason about preconditions, postconditions, and class invariants, there is no syntax in C++ that allows you to write them as part of your program. It should be noted, though, that there have been proposals to add such a syntax to the language, and it's entirely possible that this ability will be added to C++ someday in the future. There would be two benefits if we could encode this information in C++, rather than in comments:

One recent proposal that was considered for C++20 (but ultimately rejected) was one that was numbered P0542 — proposals to change the C++ standard are numbered — suggests a syntax roughly like this, though I've taken some liberties with it. Our sqrt function might be declared this way:

double sqrt(double n)
    [[expects: n >= 0.0]]
    [[ensures: sqrt(n) >= 0.0 && std::abs(sqrt(n) * sqrt(n) - n) < 0.0001]];

Notice that we've explicitly specified that the parameter n has to be non-negative; the expects attribute is suggested as a way to specify preconditions. Meanwhile, we've specified that the result of calling the function, assuming that the preconditions are met, will be non-negative, and that the square of the result will be very nearly equal to the parameter n; the ensures attribute is intended as a way to specify postconditions.

The proposal includes nothing specifically about class invariants, but you could certainly imagine such a syntax. For example, our ArrayList class might be declared this way:

class ArrayList
{
    // ...

private:
    std::string* items;
    unsigned int sz;
    unsigned int cap;
}
[[ensures: items != nullptr && cap >= sz]];

where an ensures attribute on the class could be interpreted as a class invariant. (Why I chose the ensures syntax is that a class invariant can be thought of as a postcondition of all member functions.)

Of course, it's not possible to write these kinds of things today in C++, but even though there is no syntax to support it, we have to be thinking about these things either way. Whether your language lets you encode these things explicitly or not, functions have preconditions, postconditions, and side effects; classes have invariants. Part of how we understand our own designs (and each other's) is to understand these preconditions, postconditions, side effects, and invariants.


Exceptions

In any programming language, when you call a function (or the equivalent), you're asking that function to do a job for you, given a set of parameters that configure that job. There are two possible outcomes:

There are different mechanisms for reporting failure in different programming languages, and there is not steadfast agreement in the programming language design community about how best to approach this problem, but one common approach — which appears not just in C++, but in a number of other programming languages, as well — is called an exception.

In C++, the notion of failure is handled separately from success; functions that fail don't return a value at all, but instead throw an exception. (If you've previously programmed in Python, this idea will sound familiar, as it is more or less the same as raising an exception in Python.) How you handle an exception is entirely different from how you handle the return value of a function. The basic model works this way:

Exception syntax

Syntactically, there are two things you will need to be able to do to use exceptions in a C++ program: throw them and catch them.

Throwing an exception is best done by simply creating an object of the exception's type and using it as the argument in a throw statement. For example, if you had a class called MyException that had a default constructor, we could throw a MyException this way:

throw MyException{};

Note that we're best off allocating the object statically. If we allocated it dynamically using the new operator, what we'd actually be throwing is a pointer to that object, and, even worse, we'd also be obligating the code that handles the exception to delete it.

Catching an exception is done by specifying a block of code in which you expect an exception may occur, along with an indication of what should happen if it does. This is done with a try block, which, structurally, looks like this:

try
{
    functionThatThrowsMyExceptionSometimes();
}
catch (MyException&)
{
    std::cout << "Doh!" << std::endl;
}

(It should be noted that "try block" is technically the name used to describe the whole construct, including the catch handlers, though some people also sometimes use that to refer only to the area within the try; for clarity, I'll call that the "try part".)

A try block is executed as follows:

You may have noticed that, in the examples above, exceptions are being caught by reference. This is the typical practice in C++, as it addresses two problems:

Some people also advocate catching exceptions by const reference, like this:

catch (const MyException& e)

which makes clear that you won't be modifying the exception within the catch handler. I haven't picked up that style myself, but I can see at least some benefit to it.

Why destructors should never throw exceptions, and why that makes our life simpler

As we've seen, throwing an exception sets into motion a sequence of events that includes the destruction of a potentially large number of variables — every local variable in the entire chain of functions that have failed, up until we reach a function where the exception is caught. The destruction of these variables is fully automatic and doesn't otherwise affect the process of exception propagation; it's simply a consequence of the natural unwinding of the stack.

But this does bring up an interesting question. What happens if an exception is thrown, a local variable is destroyed automatically (which means that its destructor will be called), and its destructor throws an exception? We now have two exceptions that have occurred: the one we were propagating before, as well as the additional one thrown by some destructor in the process of propagating the first one. While the details here can be subtle, the usual outcome in this case is that the program terminates immediately.

What's more, even when destructors are called because a function has exited normally and its local variables are destroyed, an exception thrown from a destructor can still cause a program crash. Unless you take special care to say otherwise, destructors in C++ have the property that they are noexcept, which means they are not permitted to throw exceptions, and that throwing one will cause the program to terminate immediately. While it's possible to turn off this default, there's rarely a reason to do so; destructors that throw exceptions would still be problematic in the case when they throw exceptions while unwinding the stack due to the propagation of another exception.

All in all, the best advice is that we should never throw exceptions in destructors, because destructors can be called in circumstances (such as the stack unwinding that happens while exceptions propagate) that will cause a program to crash immediately if the destructor throws an exception. Designing our classes so that destruction cannot fail turns out to be paramount.

Why this fact makes our life simpler is that we can make a basic assumption: If we use the delete or delete[ ] operators, it's safe to assume that they won't throw an exception. This can make it easier to design code that handles exceptions correctly and safely.


Exception safety

If you've programmed in languages like Python or Java before, a lot of this will look quite familiar; the keywords might be different (e.g., Python has a try..except statement), but the ideas are pretty similar. However, exceptions are a little thornier than they are in a lot of languages, because C++ requires manual management of resources (e.g., memory, open files, and so on). More care needs to be taken to ensure that, for example, an exception being thrown does not cause a memory leak or leave a dangling pointer in its wake.

Furthermore, once we start thinking in terms of contracts, we realize that even less complex languages like Python or Java still require us to be sure that we don't break contracts when we throw exceptions. Do the appropriate side effects happen (or are they avoided) when a function throws an exception midway through its execution? If a member function of a class fails before it ends normally, are all class invariants still preserved? We say that exception safety is the principle of ensuring that we have reasonable outcomes in cases when exceptions are thrown.

This can be an overwhelming thing to think about if you've never considered it before, but, particularly in the case of member functions of classes, the C++ Standard Library provides a good mental model of how to think about these kinds of issues. Member functions of classes like std::vector are documented to make one of the following four guarantees about what happens when an exception is thrown. (The guarantees get progressively stronger as you read further down the list.)

Ideally, our goal should be to provide the strongest of these guarantees that we can, provided that it's possible and that the cost doesn't outweigh the benefit.


The code

The official moniker for this code example is ContractsAndExceptions, so your best bet is to do this:

Alternatively, you can click the link to the tarball below: