Scenarios Requiring Exception Handling
Runtime anomalies often occur during program execution, such as:
- Division operations where the divisor is zero.
- Invalid user input, for instance, a negative value for age.
- Memory allocation failure using the
new operator due to insufficient space.
- Array index out of bounds or attempting to open a non-existent file.
Without proper detection and management, these abnormal situations frequently lead to application crashes.
Fundamentals of Exception Handling
It is crucial to understand that an exception occurring within a function can either be resolved internally within that function or delegated to its caller for resolution.
The standard syntax involves a
try block followed by one or more
catch handlers.
try {
// Code segment that may throw exceptions
}
catch (ExceptionType1 ex1) {
// Handling logic for ExceptionType1
}
catch (ExceptionType2 ex2) {
// Handling logic for ExceptionType2
}
The
try block contains the code where potential exceptions are anticipated, including
throw statements. A
throw statement is followed by an expression; the type of this expression can be a primitive type or a class object, often containing details about the error.
If we view the
catch block as a function, the exception type acts as the parameter, and the expression in
throw acts as the argument. The mechanism functions similarly to a
switch statement. Once a
throw expression is caught by a matching
catch block, subsequent
catch blocks are skipped, and execution continues after the entire handler sequence.
Basic Exception Handling Example
#include <iostream>
using namespace std;
int main() {
double dividend, divisor;
cin >> dividend >> divisor;
try {
cout << "Starting calculation." << endl;
if (divisor == 0) {
throw -1; // Throws an integer exception
} else {
cout << dividend / divisor << endl;
}
cout << "Calculation finished." << endl;
}
catch (double val) {
cout << "Caught double exception: " << val << endl;
}
catch (int err) {
cout << "Caught int exception: " << err << endl;
}
cout << "Program complete." << endl;
return 0;
}
Output (normal case):
10 2
Starting calculation.
5
Calculation finished.
Program complete.
When the divisor is not zero, no exception is thrown. The program executes the entire
try block and skips all
catch blocks.
Output (exception case):
10 0
Starting calculation.
Caught int exception: -1
Program complete.
Catching All Exceptions
To capture any type of exception regardless of its specific type, an ellipsis
... can be used in the
catch block:
catch (...) {
// Handle any exception
}
#include <iostream>
using namespace std;
int main() {
double val1, val2;
cin >> val1 >> val2;
try {
cout << "Starting process." << endl;
if (val2 == 0) {
throw -1; // Integer exception
} else if (val1 == 0) {
throw -1.0; // Double exception
} else {
cout << val1 / val2 << endl;
}
cout << "Process finished." << endl;
}
catch (double d) {
cout << "Caught double: " << d << endl;
}
catch (...) {
cout << "Caught unknown exception" << endl;
}
cout << "End of main." << endl;
return 0;
}
If
val1 is 0, a
double exception is thrown. Although both
catch(double) and
catch(...) could match it, the first specific match is chosen, similar to function overloading resolution. Since
catch(...) matches everything, it should be placed last to avoid shadowing specific handlers.
Exception Flow and Multiple Throws
If a
try block contains multiple
throw statements, execution stops at the first one thrown. Even if no matching
catch exists locally, the program will not execute subsequent
throw statements or code below them in the
try block. The control flow transfers immediately to the handler or calls
terminate if unhandled.
try {
throw 100;
throw 3.14; // This line is unreachable
}
catch (int x) {
cout << "Integer: " << x << endl;
}
catch (double y) {
cout << "Double: " << y << endl;
}
If an exception is not caught by any local
catch block, it propagates up the call stack to the caller. If it reaches the top level (
main) without being caught, the standard library function
terminate is called, typically invoking
abort to end the program.
#include <iostream>
#include <string>
using namespace std;
class CustomException {
public:
string message;
CustomException(string msg) : message(msg) {}
};
double PerformDivision(double x, double y) {
if (y == 0)
throw CustomException("Division by zero error");
cout << "Inside Division Function" << endl;
return x / y;
}
int CalculateTax(int income) {
try {
if (income < 0)
throw -1;
cout << "Calculating tax..." << endl;
}
catch (int errorCode) {
cout << "Invalid income (negative)" << endl;
}
cout << "Tax calculated." << endl;
return income * 0.15;
}
int main() {
double result = 1.2;
try {
CalculateTax(-500);
// PerformDivision throws an exception not caught inside the function
result = PerformDivision(10, 0);
cout << "End of try block" << endl;
}
catch (CustomException e) {
cout << e.message << endl;
}
cout << "Result: " << result << endl;
cout << "Execution finished." << endl;
return 0;
}
Rethrowing Exceptions
Sometimes a function may catch an exception to perform partial cleanup or logging but still needs to notify the caller that an error occurred. This is done by using
throw; without an argument inside a
catch block.
#include <iostream>
#include <string>
using namespace std;
int ProcessTax(int salary) {
try {
if (salary < 0)
throw string("Negative salary provided");
cout << "Processing tax calculation..." << endl;
}
catch (string err) {
cout << "ProcessTax Error: " << err << endl;
throw; // Rethrow the caught exception to the caller
}
cout << "Tax processed." << endl;
return salary * 0.15;
}
int main() {
try {
ProcessTax(-100);
cout << "End of try block" << endl;
}
catch (string msg) {
cout << "Main caught: " << msg << endl;
}
cout << "Done." << endl;
return 0;
}
Exception Specifications
To improve code readability and allow callers to anticipate potential errors, C++ allows specifying the types of exceptions a function might throw. This is done using an exception specification list in the function declaration or definition.
// Declaration
void TaskFunction() throw (int, double, CustomClassA, CustomClassB);
// Definition
void TaskFunction() throw (int, double, CustomClassA, CustomClassB) {
// Implementation
}
This indicates that
TaskFunction may only throw exceptions of type
int,
double,
CustomClassA, or
CustomClassB. If both declaration and definition have specifications, they must match.
An empty list indicates the function promises not to throw any exceptions:
void SafeFunction() throw ();
If no specification is provided, the function may throw any type of exception. Note that in C++11 and later, dynamic exception specifications (except
throw() which is equivalent to
noexcept) are deprecated, but older compilers might still enforce them at runtime rather than compile time.
Standard C++ Exception Classes
The C++ Standard Library provides a hierarchy of exception classes derived from
std::exception. Common classes include
bad_typeid,
bad_cast,
bad_alloc,
ios_base::failure, and
out_of_range. These classes typically include a
what() member function that returns a description of the error. Using these classes requires including
<stdexcept>.
1. bad_typeid
Thrown when the
typeid operator is applied to a polymorphic class pointer that is a null pointer.
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {};
int main() {
Base* ptr = nullptr;
try {
std::cout << typeid(*ptr).name() << std::endl;
}
catch (std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
2. bad_cast
Thrown by
dynamic_cast when a reference cast fails (e.g., trying to cast a base class reference to a derived class reference when the object is not of the derived type).
#include <iostream>
#include <stdexcept>
using namespace std;
class Base {
virtual void dummy() {}
};
class Derived : public Base {
public:
void show() { cout << "Derived method" << endl; }
};
void ProcessObject(Base& b) {
try {
Derived& d = dynamic_cast<Derived&>(b);
d.show();
}
catch (bad_cast& e) {
cerr << e.what() << endl;
}
}
int main() {
Base b;
ProcessObject(b);
return 0;
}
3. bad_alloc
Thrown by the
new operator if memory allocation fails.
#include <iostream>
#include <new>
using namespace std;
int main() {
try {
// Attempt to allocate a very large amount of memory
char* buffer = new char[0xFFFFFFFFFFFFFFF];
}
catch (bad_alloc& e) {
cerr << "Allocation failed: " << e.what() << endl;
}
return 0;
}
4. out_of_range
Thrown by methods like
std::vector::at() or
std::string::at() when an index is out of bounds. Unlike
operator[], which leads to undefined behavior on out-of-bounds access,
at() performs bounds checking and throws this exception.
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
using namespace std;
int main() {
vector<int> numbers(5);
try {
numbers.at(10) = 5; // Throws out_of_range
}
catch (out_of_range& e) {
cerr << e.what() << endl;
}
string text = "Hello";
try {
char c = text.at(10); // Throws out_of_range
}
catch (out_of_range& e) {
cerr << e.what() << endl;
}
return 0;
}