Noexcept

We saw how std::exception::what() is defined as noexcept. This means that it cannot throw an exception, or a thrown exception indicates a fatal error. Any exception that propagates out of a noexcept function calls std::terminate and ends the program. So you should declare functions that do not throw, or only throw to indicate program-terminating failures (like memory allocation failures), as noexcept. Furthermore, a noexcept function permits the compiler to optimize away error handling infrastructure for that function. So it's a good idea to declare everything that you can as noexcept.

int add(int a, int b) noexcept {
    return a + b;
}

We can also make functions conditionally noexcept. This means that the function will be noexcept if and only if some compile time expression is true.

void x() noexcept; //noexcept

template<typename T>
void f() noexcept(sizeof(T) < 4); //noexcept if sizeof(T) < 4

void k();

constexpr bool isNoexcept = noexcept(k()); //false
// using noexcept() has a conditional operator
// noexcept(f) is true if f is a noexcept callable object
// otherwise the result is false

bool t() noexcept(noexcept(f<char>)); //t is noexcept
// noexcept(f<char>) is a constant expression that evaluates to true since f<char> is noexcept
// because sizeof(char) < 4

bool s() noexcept(noexcept(f<double>)); //s is not noexcept

bool d() noexcept(noexcept(k())); //d is not noexcept

constexpr auto testNoexcept = noexcept(t()); //true

Notice how when we test if a function is noexcept, we need to have two noexcepts. This is because the inner noexcept is a conditional test, which returns true or false if the function passed is noexcept or not. The outer noexcept takes a constexpr boolean expression, and makes a function noexcept if the condition is true. Thus, when we pass the expression sizeof(T) < 4, we only need one since this is a constexpr expression.

We'll talk about templates and constexpr more later, but basically all of these conditions are determined at compile time. This incurs no runtime cost.

Exception Guarantees

There are 3:

  • Nothrow Guarantee

    This is denoted by noexcept on the function declaration. This is the strongest exception guarantee, and it doesn't necessarily mean the function does not throw. What it means is that if the function does throw, some disastrous and non-recoverable error just occurred. Use this more often than you might think, since "dead programs tell no lies"

  • Strong Guarantee

    This guarantees that an operation is atomic with respect to exceptions. The operation either completes, or an exception is thrown and nothing happened. If an operation with this guarantee fails, the state of the program will be exactly what it was before the operation.

  • Basic Guarantee

    This guarantees that if an operation does fail, no invariants are broken and the program will return to some valid state. This is the weakest of the three, and the minimum guarantee required for a correct program.

C++'s STL always offers the strong guarantee (or noexcept when denoted as such) except for range insertions (offers the basic guarantee), insert and erase on deque and vector (which are strong only if their elements' copy and assignment offer that guarantee), and the streams library (which only offers the basic guarantee).

It's good to document when a function has the strong or basic guarantee. No need to document nothrow in most cases since noexcept is self-documenting.

Destructors and move operations should be noexcept.


void logExn(const char * information, const std::exception & caughtExn) {
    std::cout << "An exception has occurred!"
        << " (" << information << ")\n"
        << caughtExn.what()
        << std::endl;
    //operator<<(std::ostream&, _) has the basic guarantee
    //so this function will only have the basic guarantee
    // Ex. An error could occur printing caughtExn.what() after
    // "An exception has occurred" has already been printed
    // furthermore, an error could occur on outputting "occurred!"
    // after "An exception has" may already have been displayed
}

void logExnImproved(const char * information, const std::exception & caughtExn) {
    std::stringstream ss;
    ss << "An exception has occurred!"
        << " (" << information << ")\n"
        << caughtExn.what();
    const auto msg = ss.str(); //convert to string
    // If any of the above fails, state remains unchanged

    std::cout << msg << std::endl; 

    // Slightly improved by buffering the message first
    // but still basic
}

/// Appends c to both outStr and outVec
void appendToBoth(std::string & outStr, std::vector<char> & outVec, char c) {
    outStr += c; // strong
    outVec.push_back(c); // strong
    /* This code is broken
     An error may occur appending c to outVec after c was already appended to outStr
     This violates the postcondition and hence our function is broken
    */
}

/// Gets a tuple of (strBuffer, vecBuffer) after c has been appended to both
auto appendToBoth2(const std::string & strBuffer, const std::vector<char> & vecBuffer, char c) {
    std::string strCpy = strBuffer; // copy ctor, strong
    std::vector vecCpy = vecBuffer; // strong
    strCpy += c;
    vecCpy.push_back(c);
    return std::make_tuple(strCpy, vecCpy);

    // Now we achieved the strong guarantee!
    // It cost 2 extra copies and a few extra moves but this isn't something to worry about
    // worry about performance only if a profiler tells you to
}

// Member function of some queue class

    /// Peek and pop the front of the queue
    /// Requires the queue is not empty
    T ppop() { 
        queue.pop(); // strong
        return t;
        // Pre C++11, this return would invoke a copy, which may throw
        // if this threw, then we would completely lose the front element since it had already been popped from the queue
        // Post C++11 this invokes a move which is noexcept

        // So for our purposes, this function is strong
    }

Exception safety shouldn't be an afterthought. We'll return to this topic again later.

Before we move on, how many paths of execution can you find in this code segment? (Answer in next chapter, from Herb Sutter's GOTW and Exceptional C++).

string EvaluateSalaryAndReturnName( Employee e )
{
    if( e.Title() == "CEO" || e.Salary() > 100000 )
    {
      cout << e.First() << " " << e.Last()
           << " is overpaid" << endl;
    }
    return e.First() + " " + e.Last();
}