Please reconsider noexcept

Author: Thorsten Ottosen
Contact: nesotto@cs.aau.dk
Organizations:The Machine Intelligence Group, Department of Computer Science, Aalborg University, Denmark
Date: 2010-11-23
Number:N3227=10-0217
Working Group:Evolution/Core

Abstract

With the adoption of n3050 we have a new keyword noexcept which allows the programmer to tell the compiler (and users) that a function throws no exceptions. This has several good consequence, in particular (a) that interfaces are more self-documenting, and (b) that the compiler can optimize code better. However, since the compiler is not allowed to deduce if a function is noexcept, the new feature places a considerable syntactical and maintenance burden on the programmer, so much in fact, that it seriously compromises a major design goal of C++0x: simplicity. We therefore urge the committee to reconsider a compiler deduced noexcept for functions which the definition is either immediate or has been previously seen.

Table of Contents

Introduction

We provide this paper as a point of reference. A preliminary paper lead to lengthy discussions during the Batavia meeting. Therefore the paper is also somewhat outdated as it does not include the discussion of the Batavia meeting.

Motivation

To get all the performance benefits of a library (which exploits the noexcept annotation with traits), e.g. the standard library, the programmer has to sprinkle noexcept all over his code. Below we give four simple examples where the use of noexcept should (in an ideal world) not be needed.

Example 1

class Foo
{
   int i;
public:
   Foo() : i(0) {}
   Foo( const Foo& r ) : i(r.i) {}
};

Why can't the compiler tell if these constructors are noexcept?

Theres also a strange difference compared to defaulted constructors, i.e., these two constructors are indeed deduced as noexcept:

class Foo
{
   int i;
public:
   Foo() = default;
   Foo( const Foo& r ) = default;
};

Example 2

template< class T >
struct numeric_limits
{
    static constexpr T max();
    ...
};    

What extra value do we get by making a constexpr function noexcept? It is true that one can construct constexpr functions that contain throw-statements, but the examples are somewhat contrived and can hardly be seen as normal. For most programmers the above interface would be cluttered with largely irrelevant information if we added noexcept to the declaration.

Example 3

Imagine a simple forwarding function. It might now look like this:

template< class T >
auto forward_with_side_effect( T& t ) noexcept( noexcept(bar(t)) && noexcept(foo(t)) ) -> decltype(foo(t))
{ 
    bar(t);
    return foo(t);
}

Do we really want to write and read code like this? It seems like a lot of key-strokes end up being devoted to something that is not of primary concern to the programmer.

Example 4

When we start to consider templates, the syntactical and maintenance burden becomes unlike anything C++ has ever seen before. The reason is the following: any operation depending on one or more template parameters potentially leaks into the declaration of the function.

Consider just something as simple as std::pair. n3157 now describes the declaration of std::pair as follows

template <class T1, class T2>
struct pair {
  typedef T1 first_type;
  typedef T2 second_type;

  T1 first;
  T2 second;
  constexpr pair() noexcept( is_nothrow_constructible<T1>::value &&
                             is_nothrow_constructible<T2>::value );
  pair(const pair&) = default;
  pair(const T1& x, const T2& y) noexcept( is_nothrow_constructible<T1, const T1&>::value &&
                                           is_nothrow_constructible<T2, const T2&>::value );
  ...
  void swap(pair& p) noexcept( noexcept(swap(first, p.first)) &&
                               noexcept(swap(second, p.second)));
};

If you think the above code is difficult to read, then imagine putting noexcept on something like std::sort().

It can hardly be argued that the above code is clear, concise or simple. Also note the strange difference between a defaulted constructor and one that is not.

The programmer ought to be able to write

template <class T1, class T2>
struct pair {
  typedef T1 first_type;
  typedef T2 second_type;

  T1 first;
  T2 second;
  constexpr pair();
  pair(const pair&) = default;
  pair(const T1& x, const T2& y);
  ...
  void swap(pair& p);
};

and get all the same benefits.

What can we do instead?

The solution appears to be quite simple: require the compiler to deduce the noexcept specification for functions without using flow-analysis.

This has already been discussed in the CWG and rejected. But the CWG did not explicitly consider the solution above, and so the discussion was based on incomplete information.

Other objections has been put forward, e.g. on the reflector. We shall review all of these objection again to give EWG/CWG a better foundation for a new discussion.

Objection 1: deducing noexcept is undecidable

Rice's theorem tells us that any non-trivial property is undecidable. Hence in general it will be impossible for a compiler to come to the right conclusion in all cases, even with whole program analysis. In turn, this implies that code might change when recompiled on a different compiler, which is highly undesirable.

Counter argument 1: do not require the compiler to do flow-analysis

Instead, the noexcept specification of an inline function is determined by a simple recursive test that all statements are noexcept, stopping the recursion at non-inline functions and built-in statements.

Objection 2: no flow-analysis implies compilation inconsistencies

There can be at least two consequences of "no flow-analysis". The first is static in nature, pertaining to compilation only.

Changing an inline function to be non-inline (or vice versa) can mean a compiling program no longer compiles because the compiler could deduce the inline version was noexcept, but the non-inline version does not specify this information in its declaration.

Then imagine a static_assert that uses noexcept somehow. The outcome of the static assertion will then change when the function is changed between inline and non-inline.

Counter argument 2: is this a major problem?

There is really no counter argument to this. It can happen. It will happen. The question is if this is a major problem, or even, if this is even significant compared to the list of problems the current proposal has.

Remark 1: From the CWG Santa Cruz wiki it appears that the CWG had the same discussion, but in the context that the compiler-deduced noexcept required whole-program analysis. This could lead to inconsistencies across compilers. This is no longer possible with the no-flow-analysis approach. The same program stays portable.

Remark 2: With the current approach client code can also be broken if the noexcept keyword is added or removed to a declaration.

Objection 3: no flow-analysis implies runtime inconsistencies

Because the outcome of noexcept can change when changing an inline function to non-inline (or vice versa), it means that different code paths gets executed in user and client code. For example, different template specializations could get instantiated, leading to untested code being executed.

Counter argument 3: again, the same program stays portable

The CWG saw this as very problematic. However, their context was again that the compiler-deduced noexcept required whole-program analysis. With the no-flow-analysis approach this is not possible anymore.

Now, in the new context, we can still get a similar problem. But this is really nothing new: there are many ways an existing program can change behavior silently if we change the program. Of course, that does not mean that we should not consider any effect of this kind.

Objection 4: if you add a "printf" some levels down your call chain (to debug a problem), that might switch an upper-level algorithm to a completely different code base

The headline says it all. If we allow the compiler to deduce noexcept the code that changes might be non-local. This change occurs if and only if

  1. at least one of the statements being added can throw
  2. all the other (old) statements in the changed function are deduced to be noexcept
  3. all outer (if any) functions in the noexcept expression in the upper-level algorithm are deduced to be noexcept.

Of course, it might happen. And it might happen that it changes code in many unforseen places.

Counter argument 4: This cascading of changes can already happen.

Of course, deducing noexcept adds one more way this can happen, but it is actually something already possible with the current C++0x draft, albeit in a slightly different manner.

Think of all the new compiler deduced type-traits. Then imagine we have a class so

class Foo
{ ... };

To begin with std::has_nothrow_copy_constructor<Foo>::value is false. Then the author of Foo changes something, and suddenly std::has_nothrow_copy_constructor<Foo>::value is true. This implies all kinds of non-local changes can happen to clients of Foo:

class Bar
{ 
    Foo foo; 
};

class FooBar
{
    Bar bar;
};

std::has_nothrow_copy_constructor<FooBar>::value might now also become true. So a cascade of non-local changes is possible with very small changes to a piece of code. In a wide range of ways, e.g. changing noexcept specifications or making classes trivially copyable.

Summary

We may summarize the problems with the current noexcept approach as follows:

The only benefit compared to the no-flow-analysis approach we can think of is that there is one less way to change client code, but as noted above, this requires changing the program. Also note that there is ample opportunity for changing client code in this way in C++0x; for example, just about any of the new compiler-supported traits has this property.

In light of the above discussion we kindly urge the CWG/EWG to reconsider the noexcept specification for functions with an immedate definition or where the definition has been previously seen.

Acknowledgements

I was prompted to write the above discussion based on a CWG reflector thread started by José Daniel García Sánchez. Bjarne Stroustrup, Jason Marrill, Dave Abrahams, David Vandevoorde, Thomas Plum, Ville Voutilainen, Martin Sebor, Christopher Jefferson, Gabriel Dos Reis, Jens Maurer, Alberto Ganesh Barbati, Daniel Krügler, Herb Sutter, Matt Austern and John Spicer were all deeply involved in the following discussions of the noexcept feature.