Exceptions are the way by which programs handle erroneous cases out of the program typical control flow. For example, a division program can handle division by zero case by throwing an exception or a dynamic memory allocation throws an exception if not enough memory available.
Without exceptions, handling these types of cases would have been nightmare as caller and the functions would have to return the results and as well provide error by some means if error occurs.
Exceptions in C++
C++ provides throw to throw exception, try block to look for exception and catch block(s) to handle the exception.
A simple program:
try
{
throw 1;
}
catch (int err)
{
std::cout << err << "\n";
}A correct matched catch block would be called and there is no implicit conversion of types happens to match the block. For example, if an int is thrown and there is a catch block for double, still the catch block would not be executed. It demands explicit types to be mentioned in the catch block.
There is only one exception with the above rule is that if a derived class object is thrown as exception and catch block is catching for base class object, the catch block would be a match.
The matching of the catch block stars from top to bottom in sequence and when a match is found, the search stops and matching catch block is executed.
catch-all handler
C++ also allows us to have a catch-all handler that catches exception of any type. Following shows an example:
try
{
throw 1;
}
catch(...)
{
// handle
}It is represented as ellipses in the catch parameter list.
Exceptions and stack unwinding
An exception can be thrown by any function which can be deep inside the call stack. When exception occurs, the stack is unwound till a matching catch block is found.
Exception classes
Although it is possible to throw exceptions by fundamental values, but they do not provide any meaningful information on why exception occurred. It is always advisable to have exception classes with a useful message with some data that might have cause the exception.
catch block can then use this exception class object and handle the exception in more meaningful manner.
For example:
int countWords(std::string_view filename)
{
if (file_not_found)
throw FileNotFoundException(filename);
// other business logic.
}
try
{
countWords("./file.txt");
}
catch (const FileNotFoundException& error)
{
// do something with the exception.
}Here, we have a custom exception class that represent exception when file does not exist.
Catching as reference or pointer
When throwing fundamental types, it really does not matter how do we catch the exception, whether by value or reference/pointer. But when we throw exceptions by class objects, we always prefer catching them using reference or pointer because it saves the unnecessary copies to be made.
std::exception
This the base class of standard exceptions to which all other standard library exceptions extends. Following section example shows the use case of this exception class.
Exceptions with dynamic allocations
If constructor is creating the object with some dynamic allocations and later during the construction if something throws an exception, the class destructor would not get called and dynamically allocated memory would not get freed.
For example:
#include <exception>
#include <stdexcept>
#include <iostream>
class Resource
{
int *ptr{};
public:
Resource()
{
ptr = new int{10};
std::cout << "Allocated the resource" << '\n';
throw std::runtime_error("Bad has happened!");
}
~Resource()
{
std::cout << "Deallocating the resource" << '\n';
delete ptr;
}
};
int main(int argc, char const *argv[])
{
try
{
Resource r;
}
catch (std::exception err)
{
std::cout << err.what() << '\n';
}
return 0;
}When we run this program, we should see following output:
Allocated the resource
std::exception
So, the destructor is not called and so dynamic memory is not deallocated.
One thing we can do is to catch the exception inside the constructor itself and deallocate the memory and then re-raise the exception.
It turns out there is a better approach to handle this by using member which can deal with dynamic memory. As member are destructed when the constructor fails, if the member is an object handling the dynamic memory would deallocate the memory when it dies. We can use smart pointers here such as std::unique_ptr.
Exceptions lifetime
When a function throws an exception, its local data is remove as the function layer from the call stack is removed. However, the exception object is not removed because C++ copies that object to a special memory apart from stack. This means that the exception class objects should be copyable, otherwise compiler would the error.
References
- https://www.learncpp.com/cpp-tutorial/basic-exception-handling/
- https://www.learncpp.com/cpp-tutorial/exceptions-functions-and-stack-unwinding/
- https://www.learncpp.com/cpp-tutorial/uncaught-exceptions-catch-all-handlers/
- https://www.learncpp.com/cpp-tutorial/exceptions-classes-and-inheritance/
- http://en.cppreference.com/w/cpp/error/exception