Acceptable Noexcept

Acceptable Noexcept

03 Sep 2018    

In this tip we’ll look at the caveats associated with noexcept.

The noexcept Guarantee

One common misconception is that noexcept implies that the function will not throw. This is wrong. Decorating a function with the noexcept specifier means that if the function does end up throwing, then it will result in a call to std::terminate and at that point, if the stack is unwound or not is left to the implementation. It does not call std::unexpected. The main difference being that std::unexpected is called when an exception of an unspecified type (declared by the function) is thrown while std::terminate is called when exception handling mechanism is violated.

Caveats

Const Member Variables

Generally const member variables create throwing move constructors. Given a simple struct:

struct Foo {
    const std::string Name;
    int Id;
};

The above struct Foo will have a throwing move constructor by default since the Name parameter’s move constructor cannot be called so it has to resort to using the throwing (noexcept(true)) copy constructor of the std::string class.

Adding Foo(Foo&&) noexcept = default; to the class declaration will result in std::terminate being called if it throws during move construction. This is of course not valid for classes that disable or have custom move members.

Noexcept and Move Constructors

Quite a few constructs, especially containers, in the STL have template overloads that check if the class provided has a throwing move constructor or not. It will perform a copy in cases where it throws, otherwise it will use move operations. For example the following is from std::vector:

template <typename T>
typename std::conditional<
  !std::is_nothrow_move_constructible<T>::value && std::is_copy_constructible<T>::value,
  T const&,
  T&&
>::type move_if_noexcept(T& t);

While this is incredibly useful for performance, it is also a means of providing a strong exception guarantee that a move operation does not modify the object (allocate / deallocate for instance).

Differences in STL Implementations

The STL libraries from different vendors don’t always have the same exception specification for their special members. Unless the standard explicitly specifies an exception specification, it is left to the library implementer to strengthen their noexcept guarantee wherever possible. For example given the following class:

class Renderer {
    std::vector<Buffer> VertexBuffers;

public:
    Renderer(Renderer&&) = default;
};

There is no guarantee that noexcept will be deduced for the move constructor because of the std::vector member variable. This is because (until C++ 17) the standard does not impose the noexcept specifier on std::vector’s move constructor. On explicitly specifying noexcept like so:

    Renderer(Renderer&&) noexcept = default;

This can cause compiler errors on different platforms since the default exception specification deduced may be different from noexcept(true).

Parameters By Copy May Throw

Let’s say you have a member function as such:

// Silly example for sake of brevity
class Symbol {
public:
    void SetName(std::string name) noexcept;
};

While it may be true that the function SetName doesn’t throw has a strong exception guarantee it may not exactly be the case. When calling SetName with a function argument that creates a copy, there is a possibility that the copy construction of std::string Name may throw. While this is something to be aware of, it is the responsibility of the calling code to ensure that this case is handled correctly.

When to use noexcept?

The best thing you can do generally is to not specify anything, leave your exception specification blank; especially if you are unsure of the feature or the function’s behaviour.

Generally noexcept should be specified for functions that perform some critical operation safely. This also allows the calling functions to be marked noexcept and so on. The good candidates for specifying noexcept are as follows:

  • Memory deallocation functions
  • Swap functions
  • Map::erase and clear functions on containers,
  • Operations on std::unique_ptr,
  • Other operations that the aforementioned functions might need to call

The Lakos Rule

The Lakos Rule is used within the STL and is a very handy guideline to use. Given the following definitions:

A wide contract for a function or operation does not specify any undefined behavior. Such a contract has no preconditions […]

A narrow contract is a contract which is not wide. Functions or operations having a narrow contract result in undefined behavior when called in a manner that violates the documented contract.

The Lakos Rule states that:

Each library function having a wide contract, that we agree cannot throw, should be marked as unconditionally noexcept.

This is a good guideline to follow. If we have a function that is extensively evaluated and checked for inputs, errors, calling functions etc and we know it’s “safe”, then it should most probably be marked noexcept.

References