C++ For C# Developers: Part 18 – Exceptions
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrapup
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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 approximate it with regular code such as a switch
. Just keep in mind that C# exception filters are evaluated before stack unwinding and this approximation is evaluated afterward:
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 try
–catch
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 catch
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!
#1 by Jamie on September 22nd, 2020 ·
It’s mad that I still see code people have slapped together that doesn’t have literally any try catches. Its mad, mad I tell you.
Great read.
Take care
Jamie
#2 by Mikant on March 14th, 2021 ·
I think it’s worth mentioning that in C# exception filters are evaluated before stack unwinding and that behaviour cannot be mimicked.
btw Great series! Thx
#3 by jackson on March 14th, 2021 ·
Excellent point! I’ve updated the article to point this out specifically.
#4 by Mark on August 8th, 2022 ·
The equivalent mechanism in C++ is to use an exception hierarchy, ie you catch on a separate type rather than a value within the exception (.e.g. https://en.cppreference.com/w/cpp/error/exception).
You write multiple catch blocks and they are evaluated in order and the first one to match the thrown type is chosen (so you should write them most->least specialized).
So e.g.
#5 by DJ on April 22nd, 2021 ·
Excellent tutorial in an excellent series!! Thanks!!
#6 by arthour on December 8th, 2023 ·
“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.”
Didn’t you mean “looking for a catch block”?
#7 by jackson on December 8th, 2023 ·
Yes, thanks for letting me know. I updated the article with a fix.