Static Noexcept Checking

C cannot be said to be a language that supports its users along all paths they may take. It is notoriously hard to write memory-safe programs (as the myriad CVEs we see every year make clear). Error handling in the C standard library relies on in-band signaling of error conditions and a very limited set of error numbers to communicate what went wrong. Indirectly called functions can either be not generic or receive void* arguments and cast them like maniacs—look no further than qsort.

But the one thing C has that actually does help programmers in a meaningful way is const.


You still gonna change that?

C++, being mostly-but-not-quite C with a lot of things bolted on, inherited the const qualifier and its wonders. In C, writing to a memory location that has been marked const is an error—but calling a function that expects an int* with a const int* will often only net you a compiler warning. In C++, that same call will be an error.

In fact, C++ has strengthened its const system so much over the version found in plain C that during the standardization of C++11 the committee discovered that const may just as well mean threadsafe. As in: any piece of data marked const may be shared freely between threads, because no thread will ever modify that piece of data. (I am oversimplifying this a bit. const_cast is a thing that exists and should never ever ever be used, and a whole host of other evil things ).

Let that sink in for a second: if your program contains some piece of data called X, and all accesses to X are made through const pointers or references, then your program cannot have a data race for X. Data races are one of the prinicipal sources of errors in multithreaded programs. Ergo, if your program is const-correct and you use constant data liberally, you can almost eliminate a large class of problems from your program.


Doesn’t race, still crashes though

Unfortunately, data races are not the only source of bad behaviour in programs. You can still pass around null pointers and dereference them by accident, which crashes your program. You can still divide by 0, which probably also crashes your program. You can summon all sorts of other nasal demons and not even realize—or worse, realize only when a new compiler version suddenly insist on the nasal demons and breaks your program in the process. You can throw exceptions and never catch them. There’s a myriad and one ways to write programs that just don’t do what they should.

Fortunately, we have tools to help us with most of these problems. Valgrind and AddressSanitizer are immensely useful for tracking memory errors like segfaults or out-of-bounds accesses. UndefinedBehaviourSanitizer helps with divide by 0 and many of the other nasal demons. Clang for has added _Nullable and _Nonnull qualifiers, which can at the very least warn about nullptr arguments to functions that don’t except them. But for exceptions, nothing of the sort seems to exist.

But not all hope is lost, we do have one small tool in the language that could help us with that: the exception specifications. Most of us will have seen at least one of these in our life and maybe wondered about it, thinking, what does throw() really mean? The throw specification, formally known as a dynamic exceptions specification, does look a lot like the throws specification that is so ubiquitous in Java, and they even do the same thing if you don’t look too closely.

In Java, void f() throws X means “f will never throw a checked exception that is not X or derived from X”. Note the checked in that sentence. Even if a function does not declare anything it might throw, it may always throw unchecked exceptions like NullPointerException. In C++ though, void f() throw(X) really does mean that a function will never throw anything not derived from X—except that it doesn’t actually mean that. What it really means is that f will crash your program if anything not derived from X was thrown from within f.

But that’s not even the whole story. If f did somehow manage to throw, say, an int, the program would not immediately abort. Instead, the runtime will call std::unexcepted, which by default calls std::terminate, which then crashes your program. You may set your own handlers for such unexpected exceptions—but those handlers, too, are expected to crash your program.

Oh, and nothing checks dynamic exceptions specifications.

It gets even better. Not only are these specifications not checked at compile time at all (at least not by any compiler I use), they aren’t even part of the type of the function they were stuck to. They are not part of the type. For that reason, they cannot be checked reliably at compile time as soon as the first function pointer is passed around.


Enter noexcept

C++11 finally realized that dynamic exception specifications are not too useful the way they are, and because they aren’t they are mostly unused or only used as little sprinkles of throw() on functinos in libraries. Dynamic exception specifications were deprecated and replaced with noexcept exception specifications, which differ from dynamic exception specifications in two major ways.

First, noexcept specs actually do guarantee that a function void f() noexcept will unconditionally crash your program instead if propagating an exception to its caller. No more std::unexpected, any violation will result to a call in std::terminate at runtime. No way out.

Second, noexcept specs are computable flags. You cannot just declare that your function does not throw exceptions. You can declare that your function does not throw exception if. You can write something like this:

template<typename T>
void f(const T& arg) noexcept(sizeof(T) > 4 && noexcept(mangle(arg)));

Given that declaration, the compiler will ensure that f<T> does not propagate any exception if sizeof(T) is greater than 4, if mangle(arg) also declares that it does not propagate any exception with a noexcept specification of its own.

Obviously, this is immensely more powerful than dynamic exception specifications, but in a slightly different way. Dynamic exceptions specifications can tell you what a function may throw, noexcept specifications can tell you whether a function may throw—the latter of which is often the most useful.

Oh, and nothing checks noexcept specifications.

It gets even better. Not only are these specifications not checked at compile time at all (at least not by any compiler I use), they aren’t even part of the type of the function they were stuck to. They are not part of the type. For that reason, they cannot be checked reliably at compile time as soon as the first function pointer is passed around. I think I’m having a déjà vu …

Until C++17, that is. In that release of the standard, noexcept specification are part of the function type, so theoretically, compilers could also check noexcept correctness. I still don’t know of any that do.


There’s no engineering like overengineering

Not having any checkers for this certainly won’t hurt anyone too badly, but it also is kind of a minor pain. Maybe if C++ had dependent types (and thus dependent function types with noexcept specs)? If we could determine whether a function can throw anything by looking not only at types, but also at values, we would have … well, something that essentially wouldn’t be C++ anymore, so let’s not fret that too much.

What this absence of checkers does do is make noexcept more dangerous than it should be. Placing a noexcept spec incorrectly can introduce crashes that might not have been there if the spec hadn’t been there either. On the other hand, liberal use of noexcept is powerful documentation, and if it is used right it can even be a guarantee to not crash. Which was the goal I had when I started looking into noexcept spec checking—using noexcept as a guarantee to not crash just like C++ uses const as a guarantee to not have data races (well, of sorts).

Out of this mess came the static-noexcept clang plugin. Get it, build it, export CXX="clang++ -fplugin=${PATH_TO_static-noexcept.so}" and you’re set!

This checker will look at, essentially, only one thing: is this code noexcept-correct? To determine whether it is, the checker will inspect all function definitions with a positive noexcept specification (this includes noexcept, noexcept(true), noexcept(X) for any other X that is true, and almost all destructors). If a function is declared as noexcept but contains any expression that could possibly throw an exception, the checker will raise an error and tell you where the offending expression is. If you are so inclined, it can even tell you which functions could be noexcept if they aren’t already.

Examples (from the test suite)

Any throw expression is immediately a violation of noexcept.

void error() noexcept { // expected-error {{is noexcept}}
	throw 1; // expected-note {{is here}}
}

Calling a function (explicitly or implicitly as constructors/destructors) that does not have a positive noexcept spec is also a violation.

void nothrow();

void fine() noexcept { // no error reported
	nothrow();
}

The checker can also look into a function definition to determine correctness if the function does not have a noexcept spec. If no definition can be found, the function is assumed to be noexcept(false), and calling it is also a violation:

struct Bad {
	Bad();
	Bad(int) {}
};

void error() noexcept { // expected-error {{is noexcept}}
	Bad b; // expected-note {{is here}}
	Bad g(0);
}

Base classes and fields of classes are checked. If a constructor is marked noexcept, all constructors called by it (explicitly or implicitly) must be noexcept. The same holds for destructors.

struct Base {
	~Derived() throw(int);
};

struct Derived : Base // expected-error {{violates destructor noexcept spec}}
{
	~Derived() noexcept {}
};

Any function that is extern "C" will automatically be assumed to be noexcept. Not doing so would break basically everything imported from C, and throwing exceptions from a function that is supposed to be called by C is just an idea that is bad enough to assume nobody really does it.

extern "C" void ext_c();

void t0() noexcept { // no error
	ext_c();
}

Any indirect calls will be treated as violations. Yes, even in C++17 mode, because I’m lazy like that.

extern "C" void c_call();
void bar() noexcept(true);

void t0() noexcept // expected-error {{is noexcept}}
{
	(&c_call)(); // expected-note {{is here}}
}

void t1() noexcept // expected-error {{is noexcept}}
{
	(&bar)(); // expected-note {{is here}}
}

Language expression other than throw are also recognized as violations if they may throw. Currently, these are some versions of dynamic_cast and typeid.

struct T0 {
	virtual ~T0() {}
};

void t0(T0& a) noexcept { // expected-error {{is noexcept}}
	(void) dynamic_cast<T1&>(a); // expected-note {{is here}}
}

void t1(T* s) noexcept { // expected-error {{is noexcept}}
	(void) typeid(*s); // expected-note {{is here}}
}

If you are absolutely, positively sure that some expression in your programm will not throw exceptions, you can instruct the checker to just ignore it.

void throws();

void t0() { // no error reported
	((void) noexcept(true), throws());
}

And finally, to round it all off, you can have the checker tell you when a function could be noexcept, but isn’t. The command line fragment to do this is truly hideous, but that’s compiler plugins for you.

// run with -Xclang -plugin-arg-static-noexcept -Xclang -Rcandidates

void t0() { // expected-remark {{could be noexcept}}
}