C++ Exceptions

C++ Exceptions

Tags
C++
Programming
Published
October 4, 2021
Author
yanbc

What are exceptions?

  • Exception was a new error handling pattern that was introduced to C++ at the beginning of the language (C++98) and has been evolving ever since.
  • The basic code structure for using exception involves three parts
    • One try block
    • One or more catch blocks
    • One or more throw statements
  • It is best to look at an example first exception_demo.cpp
    • #include <iostream> using namespace std; int main() { try { throw 20; } catch (int e) { cout << "An exception occurred. Exception #" << e << '\n'; } return 0; }
  • Note that the catch (int e) statement looks a lot like function definition that, which in this case, takes an integer as an argument.
  • In this sense, throw looks a lot like return as well. The throw statement gives control back to anther function and with it, a variable gets passed.
  • For built-in types like int and char, it is common to pass them by value. For more complex types such as class objects, it is generally more suitable to pass them by reference.
  • Here is another example throwing objects exception_ojbect.cpp
    • #include <iostream> #include <exception> #include <string> using namespace std; class MyException : public exception { private: string value; public: MyException(int v) { value = to_string(v); } const char *what() { return value.c_str(); } }; int main() { try { throw MyException(20); } catch (MyException &e) { cout << "An exception occurred. Exception Nr. " << e.what() << '\n'; } return 0; }
  • Even though we are catching a reference to an object of the MyException type, it is actually a copy of the original object being thrown.
  • The reason to use a reference is that, a base-class reference can also refer to derived-class objects. Say you have a collection of exception objects, each is thrown at different circumstances. You can then catch them all with a reference to the base exception type.
    • class BaseException { ... }; class Case1Exception : public BaseException { ... }; class Case2Exception : public BaseException { ... }; void my_function() { ... if (case1) throw Case1Exception(); if (case2) throw Case2Exception(); ... } int main() { ... try { my_function(); } catch (BaseException &be) { ... } .... return 0; }

Exceptions specification in functions

Remember when I said the throw statement looks a lot like return? Wouldn’t it be nice to include the exceptions that a function might throw in the signature, along with the return types and input variables?
Well, the good news is that it did for the early version of C++ and the official name of this feature is called exception specification. It allows function to use the throw() notation to specify the expected exception types. The bad news is that they removed it later. To be precise, the use of exceptions specification was included in C++98 and C++03, deprecated in C++11 and completely removed in C++17.
Here is an example. You can compile it with c++ -o exception_specification -std=c++98 exception_specification.cpp. Note the function signature void foo() throw(int). It says that the function foo() takes void as input, return void as output, and might throw an int exception.
#include <iostream> // A function with an exception specification using throw() void foo() throw(int) { int x = 0; if (x == 0) { throw 42; // This is allowed because we specified throw(int) } } int main() { try { foo(); } catch (int e) { std::cerr << "Caught an exception: " << e << std::endl; } return 0; }
exception_specification.cpp
The main reason why the committee decided to remove exception specification is that it is ignored by the programming community. Yes, it looks really nice and promising at first sight but if you think about it, it is useless.
Say you are working on a function that relies on a third-party library. You might be able to sort things out and specifies all the exceptions your function might throw for the this version of the library. But libraries get updated, and new exceptions are introduced. You then would have to rewrite the function to adapt to the changes. It is cumbersome and programmers are often lazy. So what will you do? You would opt not to specify the allowed exceptions in the first place.
With that said, the noexcept specifier did continue to exist in modern C++. It allows programmers to specify whether a function is allowed to throw an exception or not.
void foo() noexcept; // foo will not throw any exception void bar() noexcept(false); // bar is allowed to throw an exception

Altering the default behavior

So what happens if an exception is thrown and nobody catches it? The runtime walks up the call stack looking for a matching catch block. If it gets past main without finding one, it gives up and calls std::terminate(). The default terminate handler calls std::abort(), which ends the program on the spot. There is no guarantee that destructors run or that buffers get flushed.
std::terminate() is also called in a few other situations:
  • An exception escapes a function marked noexcept
  • An exception is thrown while another one is still in flight, e.g. a destructor throws during stack unwinding
  • throw; is used to re-throw when no exception is being handled
Here is the noexcept case noexcept_violation.cpp. Even though the call is wrapped in a try block, the catch never runs. GCC even warns you about it at compile time ('throw' will always call 'terminate').
#include <iostream> #include <stdexcept> void foo() noexcept { throw std::runtime_error("I promised I wouldn't"); } int main() { try { foo(); } catch (const std::exception &e) { // never reached: std::terminate() is called before we get here std::cout << "caught: " << e.what() << '\n'; } return 0; }
It prints
terminate called after throwing an instance of 'std::runtime_error' what(): I promised I wouldn't Aborted (core dumped)
The first two lines come from GCC's default terminate handler, which prints the exception before calling std::abort().
You cannot stop the program from ending, but you can change what happens right before it does. std::set_terminate() installs your own handler, which is a good place to write a log line or a crash report. The handler must not return. It has to end the program itself, usually by calling std::abort(). Inside the handler, std::current_exception() gives you access to the exception that caused all this. Here is an example terminate_handler.cpp
#include <cstdlib> #include <exception> #include <iostream> #include <stdexcept> void my_terminate() { // find out what was thrown, if anything if (std::exception_ptr ep = std::current_exception()) { try { std::rethrow_exception(ep); } catch (const std::exception &e) { std::cerr << "uncaught exception: " << e.what() << '\n'; } catch (...) { std::cerr << "uncaught exception of unknown type\n"; } } std::abort(); } int main() { std::set_terminate(my_terminate); throw std::runtime_error("nobody catches me"); }
It prints
uncaught exception: nobody catches me Aborted (core dumped)
Back to exception specifications for a moment. In C++98 and C++03, if a function threw something that wasn't listed in its throw() specification, the runtime called std::unexpected() instead, and you could customize that with std::set_unexpected(). Both were removed in C++17 together with the specifications themselves, so in modern C++ std::set_terminate() is the only hook left.

Working with dynamic memory allocation

new throws too

In C, malloc returns NULL when it can't allocate memory. In C++, new throws a std::bad_alloc exception instead. If you prefer the C style of checking for null, there is a nothrow version of new. Here is an example bad_alloc.cpp
#include <iostream> #include <new> int main() { // ask for 1 PiB, more than any machine has const std::size_t huge = 1ULL << 50; try { char *p = new char[huge]; delete[] p; } catch (const std::bad_alloc &e) { std::cout << "new failed: " << e.what() << '\n'; } // the nothrow version returns nullptr instead of throwing char *q = new (std::nothrow) char[huge]; if (q == nullptr) { std::cout << "new (std::nothrow) returned nullptr\n"; } delete[] q; return 0; }
It prints
new failed: std::bad_alloc new (std::nothrow) returned nullptr
One caveat on Linux: because of memory overcommit, a large but not absurd allocation usually succeeds even when there isn't enough physical memory behind it. The trouble comes later, when the pages are actually touched and the OOM killer steps in. So std::bad_alloc tells you the allocator refused. It doesn't promise that a successful new is backed by real memory.

Exceptions and memory leaks

Raw new and delete don't mix well with exceptions. When an exception is thrown, control jumps straight to the nearest matching catch block and skips every line in between, including your delete. What does get executed on the way out are the destructors of local objects. This process is called stack unwinding.
So the fix is to let a local object own the memory and release it in its destructor. This idiom is called RAII (resource acquisition is initialization), and the standard library already provides the objects you need, such as std::unique_ptr. Compare the two functions in raii.cpp
#include <iostream> #include <memory> #include <stdexcept> struct Buffer { Buffer() { std::cout << "Buffer acquired\n"; } ~Buffer() { std::cout << "Buffer released\n"; } }; void process(Buffer *) { throw std::runtime_error("processing failed"); } void leaky() { Buffer *buf = new Buffer(); process(buf); // throws delete buf; // never reached, buf is leaked } void safe() { std::unique_ptr<Buffer> buf(new Buffer()); process(buf.get()); // throws } // buf's destructor runs during stack unwinding int main() { try { leaky(); } catch (const std::exception &e) { std::cout << "leaky(): " << e.what() << "\n\n"; } try { safe(); } catch (const std::exception &e) { std::cout << "safe(): " << e.what() << '\n'; } return 0; }
It prints
Buffer acquired leaky(): processing failed Buffer acquired Buffer released safe(): processing failed
leaky() never releases its buffer. safe() does, even though it never calls delete at all. The same idea applies to every other resource: std::fstream closes files, std::lock_guard releases mutexes.
Two more rules worth remembering:
  • If a constructor throws inside a new expression, the memory is freed automatically, and the members that were already constructed are destroyed. The object's own destructor does not run, because the object was never fully constructed.
  • Don't let exceptions escape destructors. Destructors run during stack unwinding, and throwing while another exception is in flight calls std::terminate(). Since C++11, destructors are implicitly noexcept anyway, so a throwing destructor ends the program even outside of unwinding.

References