Like C#, C++ also uses exceptions as one of its main error-handling mechanisms. Today we’ll learn all about them: throwing, catching, their impact on destructors, what happens when they go uncaught, and so much more.

Table of Contents

Throwing Exceptions

The syntax for throwing an exception looks almost the same in C++ as it does in C#:

throw e;

The major difference between the two languages is that C# requires exception objects to be class instances derived from System.Exception. There is a std::exception class in the C++ Standard Library, but we’re free to ignore it and throw objects of any type: class instances, enums, primitives, pointers, etc.

class IOException {};
enum class ErrorCode { FileNotFound };
 
void Foo()
{
    // Class instance
    throw IOException{};
 
    // Enum
    throw ErrorCode::FileNotFound;
 
    // Primitive
    throw 1;
}

Note that the class instance of IOException here isn’t a pointer or reference to an instance of the class. That’s required by C# as all class instance variables are managed references. Here we throw the object itself, but we can also throw pointers if we want:

IOException ex;
 
void Foo()
{
    // Pointer to a class instance
    throw &ex;
}

It’s typical to see throw all by itself in a statement but, like in C# 7.0, it’s actually an expression that can be part of more complex statements. Here it is as commonly seen with the ternary/conditional operator:

class InvalidId{};
const int32_t MAX_PLAYERS = 4;
int32_t highScores[MAX_PLAYERS]{};
 
int32_t GetHighScore(int32_t playerId)
{
    return playerId < 0 || playerId >= MAX_PLAYERS ?
        throw InvalidId{} :
        highScores[playerId];
}
Catching Exceptions

Exceptions are caught with try and catch blocks, just like in C#:

void Foo()
{
    const int32_t id = 4;
    try
    {
        GetHighScore(id);
    }
    catch (InvalidId)
    {
        DebugLog("Invalid ID", id);
    }
}

We didn’t give the caught exception object a name in this example because the mere fact that it was thrown is sufficient for the code in the catch block. That’s also allowed in C#, as is naming the caught exception:

struct InvalidId
{
    int32_t Id;
};
const int32_t MAX_PLAYERS = 4;
int32_t highScores[MAX_PLAYERS]{};
 
int32_t GetHighScore(int32_t playerId)
{
    return playerId < 0 || playerId >= MAX_PLAYERS ?
        throw InvalidId{playerId} :
        highScores[playerId];
}
 
void Foo()
{
    try
    {
        GetHighScore(4);
    }
    catch (InvalidId ex)
    {
        DebugLog("Invalid ID", ex.Id);
    }
}

In this version, InvalidId has the ID that was invalid so we give the catch block’s exception object a name in order to access it.

Catching multiple types of exception objects also looks the same as C#:

struct InvalidId
{
    int32_t Id;
};
struct NoHighScore
{
    int32_t PlayerId;
};
const int32_t MAX_PLAYERS = 4;
int32_t highScores[MAX_PLAYERS]{-1, -1, -1, -1};
 
int32_t GetHighScore(int32_t playerId)
{
    if (playerId < 0 || playerId >= MAX_PLAYERS)
    {
        throw InvalidId{playerId};
    }
    const int32_t highScore = highScores[playerId];
    return highScore < 0 ? throw NoHighScore{playerId} : highScore;
}
 
void Foo()
{
    try
    {
        GetHighScore(2);
    }
    catch (InvalidId ex)
    {
        DebugLog("Invalid ID", ex.Id);
    }
    catch (NoHighScore ex)
    {
        DebugLog("No high score for player with ID", ex.PlayerId);
    }
}

The catch blocks are checked in the order they’re listed and the first matching type’s catch block gets executed.

Catching all types of exceptions looks a bit different in C++. We use catch (...) {} instead of just catch {}:

void Foo()
{
    try
    {
        GetHighScore(2);
    }
    catch (...)
    {
        DebugLog("Couldn't get high score");
    }
}

As in C#, we can re-throw caught exceptions, whether they have a name or not, using throw;:

void Foo()
{
    try
    {
        GetHighScore(2);
    }
    catch (...)
    {
        throw;
    }
}

C# has an exception filters feature:

catch (Exception e) when (e.Message == "kaboom")
{
    Console.WriteLine("kaboom!");
}
catch (Exception e) when (e.Message == "boom")
{
    Console.WriteLine("boom!");
}

This isn’t available in C++, but we can mimic it with regular code such as a switch:

enum class IOError { FileNotFound, PermissionDenied };
 
void Foo()
{
    try
    {
        DeleteFile("/path/to/file");
    }
    catch (IOError err)
    {
        switch (err)
        {
            case IOError::FileNotFound:
                DebugLog("file not found");
                break;
            case IOError::PermissionDenied:
                DebugLog("permission denied");
                break;
            default:
                throw;
        }
    }
}

Lastly, C++ has an alterate form of trycatch blocks that are placed at the function level:

void Foo() try
{
    GetHighScore(2);
}
catch (...)
{
    DebugLog("Couldn't get high score");
}

These are similar to a try that encompasses the whole function. The main reason to use one is to be able to catch exceptions in constructor initializer lists. Since these don’t appear in the function body, there’s no other way to write a try block that includes them.

At the time the function-level catch block is called, all constructed data members have already been destroyed. At the end of the catch, the exception is automatically re-thrown with an implicit throw; similar to the implicit return; at the of a void function:

struct HighScore
{
    int32_t Value;
 
    HighScore(int32_t playerId) try
        : Value(GetHighScore(playerId))
    {
    }
    catch (...)
    {
        DebugLog("Couldn't get high score");
    }
};
 
void Foo()
{
    try
    {
        HighScore hs{2};
    }
    catch (NoHighScore ex)
    {
        DebugLog("No high score for player", ex.PlayerId);
    }
}
 
// This prints:
// * Couldn't get high score
// * No high score for player 2

Arguments, but not local variables, can be used in a function-level catch block and they may even return:

int32_t GetHighScoreOrDefault(int32_t playerId, int32_t defaultVal) try
{
    return GetHighScore(playerId);
}
catch (...)
{
    DebugLog(
        "Couldn't get high score for", playerId,
        ". Returning default value", defaultVal);
    return defaultVal;
}
 
void Foo()
{
    DebugLog(GetHighScoreOrDefault(2, -1));
}
 
// This prints:
// * Couldn't get high score for 2. Returning default value -1
// * -1
Exception Specifications

C++ functions are classified as either non-throwing or potentially-throwing. By default, all functions are potentially-throwing except destructors and compiler-generated functions that don’t call potentially-throwing functions.

// Regular function is potentially-throwing
void Foo() {}
 
struct MyStruct
{
    // Compiler-generated constructor is non-throwing
    //MyStruct()
    //{
    //}
 
    // Destructor is non-throwing
    ~MyStruct()
    {
    }
};

This information is used by the compiler to produce more optimized code and to enable compile-time checks in case we accidentally throw in a non-throwing function.

We can override the default classification in two ways. First, by adding noexcept after the function’s argument list like where we put override or const:

void Foo() noexcept // Force non-throwing
{
    throw 1; // Compiler warning: throwing in a non-throwing function
}

We can make noexcept conditional by adding a compile-time expression in parentheses after it:

void Foo() noexcept(FOO_THROWS == 1)
{
    throw 1;
}

Compiler options such as -DFOO_THROWS=1 can be used to set FOO_THROWS to change the function’s throwing classification without changing the code.

We can also add noexcept to function pointers in the same way:

void Foo() noexcept
{
    throw 1;
}
 
void Goo() noexcept(FOO_THROWS == 1)
{
    throw 1;
}
 
void (*pFoo)() noexcept = Foo;
void (*pGoo)() noexcept(FOO_THROWS == 1) = Goo;

The second way of changing the default was deprecated in C++11 and removed completely in C++17 and C++20. It used to specify the types of exceptions that a function could throw or that a function wouldn’t throw any exceptions at all:

// Force non-throwing
// Deprecated in C++11 and removed in C++20
void Foo() throw()
{
    throw 1; // Compiler warning: this function is non-throwing
}
 
// Can throw an int or a float
// Deprecated in C++11 and removed in C++17
void Goo(int a) throw(int, float)
{
    if (a == 1)
    {
        throw 123; // Throw an int
    }
    else if (a == 2)
    {
        throw 3.14f; // Throw a float
    }
}
Stack Unwinding

Just like when we throw exceptions in C#, exceptions thrown in C++ unwind the call stack looking for a try block that can handle the exception. This triggers finally blocks in C#, but C++ doesn’t have finally blocks. Instead, destructors of local variables are called without the need for any explicit syntax such as finally:

struct File
{
    FILE* handle;
 
    File(const char* path)
    {
        handle = fopen(path, "r");
    }
 
    ~File()
    {
        fclose(handle);
    }
 
    void Write(int32_t val)
    {
        fwrite(&val, sizeof(val), 1, handle);
    }
};
 
void Foo()
{
    File file{"/path/to/file"};
    int32_t highScore = GetHighScore(123);
    file.Write(highScore);
}

If GetHighScore throws an exception in this example, the destructor of file will be called and the file handle will be closed and relinquished to the OS. If GetHighScore doesn’t throw an exception, the lifetime of file will come to an end at the end of the function and its destructor will be called. In either case, a resource leak is prevented and no try or finally block needs to be written.

As the call stack is unwound looking for suitable catch blocks, we may reach the root function of the call stack and still not have caught the exception. In this case, the C++ Standard Library function std::terminate is called. This calls the std::terminate_handler function pointer. It defaults to a function that calls std::abort, which effectively crashes the program. We can set our own std::terminate_handler function pointer, typically to perform some kind of crash reporting before calling std::abort:

void SaveCrashReport()
{
    // ...
}
 
void OnTerminate()
{
    SaveCrashReport();
    std::abort();
}
 
std::set_terminate(OnTerminate);
 
// ... anywhere else in the program ...
 
throw 123; // calls OnTerminate if not caught

std::terminate is also called in many other circumstances. One of these is if a destructor called during stack unwinding itself throws an exception:

struct Boom
{
    ~Boom() noexcept(false) // Force potentially-throwing
    {
        DebugLog("boom!");
 
        // If called during stack unwinding, this calls std::terminate
        // Otherwise, it just throws like normal
        throw 123;
    }
};
 
void Foo()
{
    try
    {
        Boom boom{};
        throw 456; // Calls boom's destructor
    }
    catch (...)
    {
        DebugLog("never printed");
    }
}

Another way std::terminate could be called is if a non-throwing function throws:

struct Boom
{
    ~Boom() // Non-throwing
    {
        throw 123; // Compiler warning: throwing in non-throwing function
    }
};
 
void Foo()
{
    try
    {
        Boom boom{};
    }
    catch (...)
    {
        DebugLog("never printed");
    }
}

Or if a static variable’s constructor throws an exception:

struct Boom
{
    Boom()
    {
        throw 123;
    }
};
 
static Boom boom{};

Note that throwing in a static local variables’ constructor doesn’t call std::terminate. Instead, the constructor is just called again the next time the function is called:

struct Boom
{
    Boom()
    {
        throw 123;
    }
};
 
void Goo()
{
    static Boom boom{}; // Static local variable who's constructor throws
}
 
void Foo()
{
    for (int i = 0; i < 3; ++i)
    {
        try
        {
            Goo();
        }
        catch (...)
        {
            DebugLog("caught"); // Prints three times
        }
    }
}
Slicing

One common mistake is to catch class instances that are part of an inheritance hierarchy. We typically want to catch the base class (IOError) to implicitly catch all the derived classes (FileNotFound, PermissionDenied). This will lead to “slicing” off the base class sub-object of the derived class. Since the subobject is really designed to be used as a part of the derived class object, this may cause errors.

To see this in action, consider the following case where virtual functions aren’t respected:

struct Exception
{
    const char* Message;
 
    virtual void Print()
    {
        DebugLog(Message);
    }
};
 
struct IOException : Exception
{
    const char* Path;
 
    IOException(const char* path, const char* message)
    {
        Path = path;
        Message = message;
    }
 
    virtual void Print() override
    {
        DebugLog(Message, Path);
    }
};
 
FILE* OpenLogFile(const char* path)
{
    FILE* handle = fopen(path, "r");
    return handle == nullptr ? throw IOException{path, "Can't open"} : handle;
}
 
void Foo()
{
    try
    {
        FILE* handle = OpenLogFile("/path/to/log/file");
        // ... use handle
    }
    // Catching the base class slices it off from the whole IOException
    catch (Exception ex)
    {
        // Calls Exception's Print, not IOException's Print
        ex.Print();
    }
}

To fix the issue, simply catch by reference:

catch (Exception& ex)

A const reference is usually even better since it’s rare to want to modify the exception object:

catch (const Exception& ex)

Either way, the appropriate virtual function will now be called and we’ll get the right error message:

Can't open /path/to/log/file
Conclusion

Exceptions in C++ are broadly similar to exceptions in C#. Both languages can throw class instances and catch one, many, or arbitrary types. C++ lacks catch filters, but emulates it with normal code like switch statements. It lacks finally because destructors are used instead without the need to remember to add try or finally blocks.

C++ also gains the ability to throw objects, not just references. Those objects don’t have to be class instances as primitives, enums, and pointers are also allowed. We can also gain a compiler safety net and better optimization by using noexcept specifications. When exceptions go uncaught, we can hook into std::terminate_handler to add crash reporting or take any other actions before the program exits.

That’s it for exceptions! As we go through the series we’ll see how they’re incorporated into other language features such as dynamic allocation and runtime casts. Stay tuned!