Exception Handling
Exception handling is the process where we deal with exceptional cases/edge cases. In general, exceptions are always preferred over using error codes in terms of program design. For example, consider the following example:
int milk, donuts;
cin << milk << donuts;
double dpg = donuts / static_cast<double>(milk);
cout << dpg;
This code assumes that
milk != 0
. If
milk == 0
, then there be an error due to division by zero. One way to handle this is by if statements. The other way is using
try-catch blocks.
Try-Catch
try {
int milk, donuts;
cin << milk << donuts;
if (milk <= 0) throw donuts;
double dpg = donuts / static_cast<double>(milk);
cout << dpg;
} catch(int e) {
cout << "We have " << e << " donuts, and no milk!" << endl;
}
Try Block
The
try block contains the code that tells the program what to do when everything goes smoothly. If something unusual happens, we throw an exception, and flow of control is transferred to the catch block.
Catch Block
The
catch block contains code that executes when the try block throws an exception. Executing the catch block is known as "catching the exception" or "handling the exception". The catch block takes in a catch-block parameter, which is preceded by a type name that specifies the type of thrown value that the catch block can catch. The catch-block parameter also gives a name for the thrown value to be used in the catch block. If no parameter is needed, the variable name can be left out (see
below).
catch(...)
is a special kind of
catch
block that can be used to catch a thrown value of any type.
Nested Try-Catch Blocks
If an exception is thrown in the inner try block but not caught in any of the inner catch blocks, then the exception is thrown to the outer try block and possibly caught by an outer catch block.
Defining Your Own Exception Class
It is common to define a class whose objects can carry precise info on what is thrown to the catch block. This allows us to have a different type to identify each possible kind of exceptional situation. An exception class is just a class, for example:
class NoMilk {
public:
NoMilk(){}
NoMilk(int num) : count(num) {}
int getCount() const {return count;}
private:
int count;
}
int main () {
try {
int milk, donuts;
cin << milk << donuts;
if (milk <= 0) throw NoMilk(donuts);
double dpg = donuts / static_cast<double>(milk);
cout << dpg;
} catch(NoMilk e) {
cout << "We have " << e.getCount() << " donuts, and no milk!" << endl;
}
}
Trivial Exception Class
Exception classes with no member data can be defined as it allows us to throw an object of that class and activate the appropriate catch block. e.g.
class DivideByZero{};
Multiple Throws And Catches
A try block can throw any numnber of exception values. When catching multiple exceptions the order of the catch blocks can be important. The catch blocks are tried in order, so it is recommended to place more specific exceptions first and more generic exceptions last.
try {
int milk, donuts;
cin << milk << donuts;
if (milk < 0) throw NegativeNumber("milk");
if (milk == 0) throw DivideByZero();
double dpg = donuts / static_cast<double>(milk);
cout << dpg;
} catch(NegativeNumber e) {
cout << "Cannot have a negative number of " << e.getMessage() << endl;
} catch (DivideByZero) { // no need to give parameter if not used
cout << "ERROR: DIVIDE BY ZERO!";
}
Throwing Exceptions In Functions
If we want to define the catch block outside of a function, the function can throw an exception that is handled when the function is called within a try block.
try {
quotient = safeDivide(num, denom);
} catch(DivideByZero) {
cout << "ERROR: DIVSION BY ZERO!";
}
// Function throws to outer try-catch block
double safeDivide(int top, int bot) throw (DivideByZero) {
if (bottom == 0) throw DivideByZero();
return top / static_cast (bottom);
}
Exception Specification
The
exception specification, also known as the
throw list, lists the types of exceptions that the function might throw but does not handle/catch. Exception specifications appear in both the function declaration and definition and if there are multiple function declarations, all of them must have identical exception specifications. If there are multiple possible exceptions, the exception types are separated by commas. i.e.
void foo() throw (DivideByZero, SomeOtherException);
If a function should not throw any exceptions that are caught inside the function, we use an empty exception specification,
void foo() throw ();
Any exceptions that are not included in the throw list invoke
unexpected()
. The behaviour of
unexpected()
can be changed with
set_unexpected()
(which takes in the new function as its parameter), but its default action is to call
terminate()
which ends the program.
In Practice
Exception specifications have no been proven useful in practice:
-
Run-time checking:
- exception specifications are checked at runtime rather than at compile time. Does not guarantee that all exceptions have been handled.
- unhandled exceptions call
std::unexpected()
which defaults to terminating the program.
-
Run-time overhead:
- Run-time checking requires the compiler to produce additional code. Affects optimizations.
-
Unusable in generic code:
- not generally possible to know what types of exceptions may be thrown, so a precise exception specification cannot be written.
In practice, only two forms of exception-throwing guarantees are useful:
- might throw any exception: omit exception-specification
- will never throw any exception: noexcept (see below)
noexcept
In C++11, throw
has been deprecated. Instead we use noexcept()
which takes in an expression (but does not evaluate it).
If a function might throw an exception, we omit any throw statement or use noexcept(false)
If a function does not throw, use noexcept
or noexcept(true)
e.g. void f() noexcept; // f does not throw
If you know a function will never throw or propagate an exception, declare it
noexcept
because it will facilitate optimization of your code.
Inheritance
Exception class hierarchies are useful.
If we have a derived class
D
of class
B
, and
B
is in the exception specification, then a thrown
D
will be caught.
Programming Techniques For Exception Handling
-
Separate throwing and catching into separate functions - throw within a function definition and list it in the exception specification, but place the catch clause in a different function
-
Never let the destructor throw an exception as it might be called when dealing with another exception
-
Exceptions should be used sparingly as they alter the flow of control. As a good rule of thumb, if a throw is desired, consider how to do it without the throw and if reasonable, do that instead.
4 Levels of Exception Safety
There are 4 levels of exception safety:
-
No Guarantee
- If an exception occurs, it will not be handled
- Memory can be leaked
- Class invariants can be violated
class A {
int *i;
A(int *i) : i { i } {}
~A() { delete i; }
}
If an error occurs and an exception is thrown with an object of type A
, the destructor will not be called and there will be a memory leak. This does no have any exception handling thus it has no guarantee.
-
Basic Guarantee
- If an exception occurs, the program is valid, but in an unspecified state
- Nothing is leaked
- Class invariants are maintained
- e.g. Regular copy constructor where data is lost
e.g.
class A {}
class B {}
class C {
A a;
B b;
public:
void f() {
a.method1();
b.method2();
}
};
If a.method1()
runs and b.method2()
has an error, the state of a
is changed without b
changing. This results in a partial function.
-
Strong Guarantee
- Exception is raised while executing error
- The state of the program will be as if the function had not been called
- e.g. Copy and Swap idiom
e.g.
class C {
A a;
B b;
public:
void f() {
A aTemp = a; // calls copy ctor
B bTemp = b;
aTemp.method1();
bTemp.method2();
std::swap(a, aTemp); // nothrow
std::swap(b, bTemp); // nothrow
}
}
If we had used a copy assignment operator instead of swap
, we would not have a strong exception guarantee because copy assignment operator could throw. The non-throwing swap
ensures that the state of a
and b
are not changed if the above methods fail.
-
Nothrow Guarantee
- Function will never throw an exception
- no exception is emitted outside of the function
- Always accomplishes its task
- use
noexcept
A possible further enhancement to the above example with strong guarantee is to use pointers via the pImpl idiom as copying pointers cannot throw exceptions. This provides a nothrow guarantee. i.e.
struct Cimpl {
A a;
B b;
}
class C {
unique_ptr pImpl; // pointer to implementation
public:
auto temp = make_unique(*pImpl);
// if temp == nullptr, nothing happens
temp->a.method1();
temp->b.method2();
std::swap(pImpl, temp);
}; // Nothrow
STL Vectors
Vectors (more on vectors
here) encapsulate heal-allocated arrays. This is a form of the
RAII idiom because when the stack-allocated vector goes out of scope, the internal heap-allocated array is freed.
void f() { vector v; }
When
v
goes out of scope, the
MyClass
destructor runs on all items in the array and the array is freed.
void g(){ vector v; }
When
v
goes out of scope, pointers dont have destructors, so only the array is freed while pointers are not. To fix this, we can use shared pointers:
void h() { vector> v; }
Emplace Back
vector<T>::emplace_back
offers strong guarantee because if the array is full, it:
- allocates a new, larger array
- copy the objects over via copy constructor
- If copy constructor throws:
- destroys new array
- old array is intact
- strong guarantee
- else delete old array
Consider the move constructor. With the move constructor,
emplace_back
will:
- allocate new larger array
- move the objects over
- delete old array
The problem is that if the move constructor were to throw,
emplace_back
wouldn't offer strong guarantee because the original array would no longer be intact. However,
emplace_back
promises the strong guarantee, so if the move constructor offers nothrow guarantee,
emplace_back
will use it, otherwise it will use the copy constructor which may be slower.
Rule of Thumb
Move operations should provide nothrow guarantee if possible and should indicate that they do so.
class MyClass {
public:
MyClass(MyClass &&other) noexcept {}
MyClass &operator=(MyClass &&other) noexcept {}
}