Exceptions Exceptions Exceptions
Intro
What Are Exceptions?
objects that can be thrown upwards
the call hierarchy
- throwing transfers control back to the caller of the current function
- they can be
caught
/ handled viatry … catch
blocks - if not handled, exceptions propagate up until they reach
main
- if an exception is not handled in
main
⇒std::terminate
will be called - default behavior of
std::terminate
is to abort the program
Example
Original motivation for exceptions
was to report the failure of a constructor to properly initialize an object, i.e., failure to establish the required class invariants (a constructor does not have a return type that could be used for error reporting)
#include <iostream>
#include <stdexcept> // standard exception types
class Fraction {
int numer_;
int denom_;
public:
explicit constexpr
Fraction (int numerator, int denominator):
numer_{numerator}, denom_{denominator}
{
if (denom_ == 0)
throw std::invalid_argument{ "denominator must not be zero"};
}
…
};
int main () {
try {
int d = 1;
std::cout << "enter a denominator: ";
std::cin >> d;
Fraction f {1,d};
…
}
catch (std::invalid_argument const& e) {
// deal with / report error here
std::cerr << "error: " << e.what() << '\n';
}
…
}
Usages
1. Precondition Violations
- Precondition = expectation regarding inputs (valid function arguments)
- Violation Examples: out-of-bounds container index / negative number for square root
Wide Contract
Functions perform precondition checks before using their input valuesThese are usually not used in performance-critical code where one does not want to pay the cost of input validity checks if passed-in arguments are already known to be valid.
2. Failure To Establish / Preserve Invariants
- Public member function fails to set valid member values
- Example: out of memory during vector growth
3. Postcondition Violations
- Postcondition = expectation regarding outputs (return values)
- Violation = function fails to produce valid return value or corrupts global state
- Examples:
- constructor fails
- can't return result of division by zero
Advantages / Disadvantages of Exceptions
- separation of error handling code from business logic
- centralization of error handling (higher up the call chain)
- nowadays negligible performance impact when no exception is thrown
- but, usually performance impact when exception is thrown
- performance impact due to extra validity checks
- easy to produce resource/memory leaks (more below)
Alternatives
Input Value Is Invalid (Precondition Violation)
narrow contract
functions: make sure arguments are valid before passing them- use parameter types that preclude invalid values
- this is preferred nowadays for better performance
Failed To Establish / Preserve Invariants
- error states / flags
- set object to special, invalid value / state
Can't Return Valid Value (Postcondition Violation)
- return error code via separate output parameter (reference or pointer)
- return special, invalid value
- use special vocabulary type that can either contain a valid result or
nothing
, like C++17'sstd::optional
or Haskell'sMaybe
Standard
Exceptions are one of the few places where the C++ standard library uses inheritance:
All standard exception types are subtypes of std::exception
.
std::exception
↑ logic_error
| ↑ invalid_argument
| ↑ domain_error
| ↑ length_error
| ↑ out_of_range
| …
↑ runtime_error
↑ range_error
↑ overflow_error
↑ underflow_error
…
try {
throw std::domain_error{
"Error Text"};
}
catch (std::invalid_argument const& e) {
// handle only 'invalid_argument'
…
}
// catches all other std exceptions
catch (std::exception const& e) {
std::cout << e.what()
// prints "Error Text"
}
Some standard library containers offer wide contract
functions
that report invalid input values by throwing exceptions:
std::vector<int> v {0,1,2,3,4};
// narrow contract:// no checks, max performance
int a = v[6]; // UNDEFINED BEHAVIOR
// wide contract:// checks if out of bounds
int b = v.at(6); // throws std::out_of_range
Re-Throwing
try {
// potentially throwing code
}
catch (std::exception const&) {
throw; // re-throw exception(s)
}
Catching All Exceptions
try {
// potentially throwing code
}
catch (...) {
// handle failure
}
Centralize
!- avoids code duplication if same exception types are thrown in many different places
- useful for converting exceptions into error codes
void handle_init_errors () {
try { throw; // re-throw! }
catch (err::device_unreachable const& e) { … }
catch (err::bad_connection const& e) { … }
catch (err::bad_protocol const& e) { … }
}
void initialize_server (…) {
try {
…
} catch (...) { handle_init_errors(); }
}
void initialize_clients (…) {
try {
…
} catch (...) { handle_init_errors(); }
}
Problems
Leaks
Almost any piece of code might throw exceptions
⇒ heavy impact on design of C++ types and libraries
Potential source of subtle resource/memory leaks, if used with
- external C libraries that do their own memory management
- (poorly designed) C++ libraries that dont't use RAII for automatic resource management
- (poorly designed) types that don't clean up their resources on destruction
Example: Leak due to C-style resource handling
i.e., two separate functions for resource initialization (connect) and finalization (disconnect)
void add_to_database (database const& db, std::string_view filename) {
DBHandle h = open_dabase_conncection(db);
auto f = open_file(filename);
// if 'open_file' throws ⇒ connection not closed!
// do work…
close_database_connection(h);
// ↑ not reached if 'open_file' threw
}
What's RAII again?
- constructor: resource acquisition
- destructor: resource release/finalization
If exception is thrown
- objects in local scope destroyed: destructors called
- with RAII: resources properly released/finalized
class DBConnector {
DBHandle handle_;
public:
explicit
DBConnector (Database& db):
handle_{make_database_connection(db)} {}
~DBConnector () { close_database_connection(handle_); }
// make connector non-copyable:
DBConnector (DBConnector const&) = delete;
DBConnector& operator = (DBConnector const&) = delete;
};
void add_to_database (database const& db, std::string_view filename) {
DBConnector(db);
auto f = open_file(filename);
// if 'open_file' throws ⇒ connection closed!
// do work normally…
} // connection closed!
Write an RAII wrapper if you have to use a library (e.g., from C) that employs separate functions for initilization and finalization of resources.
Often, it also makes sense to make your wrapper non-copyable (delete the copy constructor and copy assignment operator), especially if one has no control over the referenced external resources.
… Don't let exceptions escape from destructors or resources may be leaked!
class E {
public:
~E () {
// throwing code ⇒ BAD!
}
…
};
class A {
// some members:
G g; F f; E e; D d; C c; B b;
…
};
In Destructors:
Catch Exceptions From Potentially Throwing Code!
class MyType {
public:
~MyType () { …
try {
// y throwing code…
} catch ( /* … */ ) {
// handle exceptions…
} …
}
};
in case an exception is thrown:
No Guarantee
must be assumed of any C++ code unless its documentation says otherwise:
- operations may fail
- resources may be leaked
- invariants may be violated (= members may contain invalid values)
- partial execution of failed operations may cause side effects (e.g. output)
- exceptions may propagate outwards
Basic Guarantee
- invariants are preserved, no resources are leaked
- all members will contain valid values
- partial execution of failed operations may cause side effects (e.g., values might have been written to file)
This is the least you should aim for!
Strong Guarantee (commit or rollback semantics
)
- operations can fail, but will have no observable side effects
- all members retain their original values
Memory-allocating containers should provide this guarantee, i.e., containers should remain valid and unchanged if memory allocation during growth fails.
No-Throw Guarantee (strongest)
- operations are guaranteed to succeed
- exceptions not observable from outside (either none thrown or caught internally)
- documented and enforced with
noexcept
keyword
Prefer this in high performance code and on resource constrained devices.
No-Throw Guarantee: noexcept
noexcept
noexcept
C++11
void foo () noexcept { … }
'foo'
promises to never throw exceptions / let any escape- if an exception escapes from a noexcept function anyway ⇒ program will be terminated
Think carefully if you can keep the no-throw promise!
- noexcept is part of a function's interface (even part of a function's type as of C++17)
- changing noexcept functions back into throwing ones later might break calling code that relies on not having to handle exceptions
constexpr int N = 5;
// 'foo' is noexcept if N < 9
void foo () noexcept( N < 9 ) { … }
// 'bar' is noexcept if foo is
void bar () noexcept( noexcept(foo()) ) {
…
foo();
…
}
are all implicitly-declared special members
- default constructors
- destructors
- copy constructors, move constructors
- copy-assignment operators, move-assignment operators
- inherited constructors
- user-defined destructors
unless
- they are required to call a function that is noexcept(false)
- an explicit declaration says otherwise
Uncaught exception in main
std::terminate
is called- which calls the termination handler
- which by default calls
std::abort
and thereby terminates the program normally
Set custom handler
std::set_terminate(handler);
sets the function(object) that is called by std::terminate
#include <stdexcept>
#include <iostream>
void my_handler () {
std::cerr << "Unhandled Exception!\n";
std::abort(); // terminate program
}
int main () {
std::set_terminate(my_handler);
…
throw std::exception{};
…
}
$ g++ main.cpp -o test
$ ./test
Unhandled Exception!
std::current_exception
- captures the current exception object
- returns a
std::exception_ptr
referring to that exception - if there's no exception ⇒ an empty std::exception_ptr is returned
std::exception_ptr
- either holds a copy or a reference to an exception
std::rethrow_exception(exception_ptr)
- throws an exception object referred to by an exception pointer
#include <exception>
#include <stdexcept>
void handle_init_errors ( std::exception_ptr eptr) {
try {
if (eptr) std::rethrow_exception(eptr);
}
catch (err::bad_connection const& e) { … }
catch (err::bad_protocol const& e) { … }
}
void initialize_client () {
if (…) throw err::bad_connection; …
}
int main () {
std::exception_ptr eptr;
try {
initialize_client(); …
} catch (...) {
eptr = std::current_exception();
}
handle(eptr);
} // eptr destroyed // ⇒ captured exceptions destroyed
std::uncaught_exceptions
returns the number of currently unhandled exceptions in the current thread
#include <exception>
void foo () {
bar(); // might have thrown
int count = std::uncaught_exceptions();
…
}
Comments…