Exception Handling in C++: Concepts and Implementation
Traditional error handling in C relies on return values or global variables to indicate function execution status, but this approach has significant drawbacks. Terminating the program using functions like assert or exit leads to abrupt crashes, disrupting stability. For example, a calculator should not crash when dividing by zero but should report an error. Returning error codes requires developers to manual check and interpret them, which is cumbersome.
C++ addresses these limitations by introducing exceptions, which do not terminate the program and provide detailed error information.
Exceptions are an object-oriented method for error handling. When a function encounters an error it cannot handle, it throws an exception, allowing the direct or indirect caller to manage it. C++ provides key keywords for exception handling:
throw: Used to throw an exception when an issue arises.catch: Used to catch exceptions; multiplecatchblocks can be defined.try: Contains code that might throw exceptions, typically followed by one or morecatchblocks.
Note: Any type of object can be thrown. Thrown exceptions must be caught. try and catch must be used together; the catch block executes only if an exception is thrown, otherwise it is skipped.
Example of division by zero:
#include <iostream>
using namespace std;
int computeQuotient() {
int numerator, denominator;
cin >> numerator >> denominator;
if (denominator == 0) {
throw "Denominator cannot be zero";
}
return numerator / denominator;
}
int main() {
try {
cout << computeQuotient() << endl;
}
catch (const char* errorMessage) {
cout << errorMessage << endl;
}
return 0;
}
Exception Throwing and Matching Rules
- Exceptions are triggered by throwing an object with
throw, and the object's type determines whichcatchblock is activated.
#include <iostream>
using namespace std;
int computeQuotient() {
int numerator, denominator;
cin >> numerator >> denominator;
if (denominator == 0) {
throw "Denominator cannot be zero";
}
return numerator / denominator;
}
int main() {
try {
cout << computeQuotient() << endl;
}
catch (const char* errorMessage) {
cout << errorMessage << endl;
}
catch (int errorCode) {
cout << errorCode << endl;
}
return 0;
}
In this code, the thrown object is a string type, so it activates the first catch block, outputting "Denominator cannot be zero".
Note: If the catch type does not match the thrown object type, catching fails, and the compiler reports an error. C++ requires that thrown exceptions must be caught.
- The selected handler is the one in the call chain that matches the object type and is closest to the throw point.
Stack Unwinding Matching Principle in Function Call Chains
- First, check if
throwis inside atryblock; if so, search for a matchingcatch. If found, transfer control to thatcatchfor handling. If no match is found, unwind the current function stack and continue searching in the caller's stack. If no match is found by the timemainis reached, terminate the program and report an error. This process is called stack unwinding. - After finding and handling a matching
catch, execution continues after thecatchblock.
- It is impractical to anticipate all exceptions in code, and uncaught exceptions can cause program termination, affecting user experience.
catch(...)can catch any type of exception (three dots represent any exception), but the drawback is not knowing the specific error. (This is the last line of defense against program termination.)
However, catch(...) must be placed last in the order of catch blocks.
-
Can thrown objects be of type
string? Sincestringobjects call destructors when they go out of scope, cancatchstill catch the exception? Yes. After throwing an exception object, a copy of the exception object is created because the thrown object might be temporary. This copied temporary object is destroyed after being caught. (This handling is similar to function value return.) -
In practice, matching rules have an exception: derived class objects can be thrown and caught using base class references, which is very practical and will be explained in detail later.
Exception Rethrowing
A single catch block might not fully handle an expection. After some corrective processing, it may need to pass the exception to a higher-level function for further handling by rethrowing it.
Example:
#include <iostream>
using namespace std;
double divideValues(int x, int y) {
if (y == 0) {
throw "Division by zero error";
}
return x / y;
}
void processFunction() {
int* dataArray = new int[10];
try {
int val1, val2;
cin >> val1 >> val2;
cout << divideValues(val1, val2) << endl;
}
catch (const char* errorMsg) {
cout << "Deleting dataArray at " << dataArray << endl;
delete[] dataArray;
throw errorMsg;
}
cout << "Deleting dataArray at " << dataArray << endl;
delete[] dataArray;
}
int main() {
try {
processFunction();
}
catch (const char* e) {
cout << e << endl;
}
return 0;
}
Here, the exception is caught but not fully handled; it is rethrown for external processing.
Exception Specificasions
In practice, exceptions have the drawback of causing execution flow jumps. If an exception is nested through multiple function layers, once thrown, significant issues can arise.
- Exception specifications aim to inform function users about possible exceptions. You can list all exception types a function might throw after the function with
throw(type). throw()after a function endicates it does not throw exceptions.- Without an exception interface declaration, the function can throw any type of exception.
However, this specification is not enforced.
// This function may throw exceptions of types A, B, C, or D
void exampleFunction() throw(A, B, C, D);
// This function only throws bad_alloc exceptions
void* operator new (std::size_t size) throw (std::bad_alloc);
// This function does not throw exceptions
void* operator delete (std::size_t size, void* ptr) throw();
// C++11新增的noexcept,表示不抛异常
thread() noexcept;
thread (thread&& x) noexcept;
Custom Exception System
In practical applications, simply throwing a number or string has little meaning. We generally throw a custom type. This type typically has an id (usually representing an error code) and detailed error information.
class CustomException {
public:
CustomException(const string& message, int code)
:errorMessage(message), errorCode(code) {}
virtual string getMessage() const {
return errorMessage;
}
protected:
string errorMessage;
int errorCode;
};
For example, error code 1 might indicate insufficient permissions, code 2 server failure, code 3 network error, etc.
Below is a simple example of sending a message; if sending fails, check if it's a network error. If so, continue to resend; otherwise, log and end.
void transmitMessages() {
while (attempts--) {
try {
sendMessage(msg);
}
catch (const CustomException& e) {
if (e.getCode() == 3) {
continue;
}
else {
// Log error
logError();
break;
}
}
}
}
In practice, it's more complex due to multiple modules like network, cache, and database. Each module has its own exception handling. Using a single structure is too broad. Therefore, we throw derived class objects and catch them using base class references.
Below is a simplified server exception implementation:
class CustomException {
public:
CustomException(const string& message, int code)
:errorMessage(message), errorCode(code) {}
virtual string getMessage() const {
return errorMessage;
}
protected:
string errorMessage;
int errorCode;
};
class DatabaseException : public CustomException {
public:
DatabaseException(const string& message, int code, const string& query)
:CustomException(message, code), sqlQuery(query) {}
virtual string getMessage() const {
string result = "DatabaseException:";
result += errorMessage;
result += "->";
result += sqlQuery;
return result;
}
private:
const string sqlQuery;
};
class CacheException : public CustomException {
public:
CacheException(const string& message, int code)
:CustomException(message, code) {}
virtual string getMessage() const {
string result = "CacheException:";
result += errorMessage;
return result;
}
};
class ServerException : public CustomException {
public:
ServerException(const string& message, int code, const string& reqType)
:CustomException(message, code), requestType(reqType) {}
virtual string getMessage() const {
string result = "ServerException:";
result += requestType;
result += ":";
result += errorMessage;
return result;
}
private:
const string requestType;
};
void manageDatabase() {
srand(time(0));
if (rand() % 7 == 0) {
throw DatabaseException("Insufficient permissions", 100, "select * from name = 'ZhangSan'");
}
cout << "Execution successful" << endl;
}
void manageCache() {
srand(time(0));
if (rand() % 5 == 0) {
throw CacheException("Insufficient permissions", 100);
}
else if (rand() % 6 == 0) {
throw CacheException("Data does not exist", 101);
}
manageDatabase();
}
void handleServerRequest() {
// ...
srand(time(0));
if (rand() % 3 == 0) {
throw ServerException("Requested resource does not exist", 100, "get");
}
else if (rand() % 4 == 0) {
throw ServerException("Insufficient permissions", 101, "post");
}
manageCache();
}
int main() {
while (1) {
Sleep(500);
try {
handleServerRequest();
}
catch (const CustomException& e) { // Catching base class objects works here
// Polymorphism
cout << e.getMessage() << endl;
}
catch (...) {
cout << "Unknown Exception" << endl;
}
}
return 0;
}
Advantages and Disadvantages of C++ Exceptions
Advantages:
- Exception objects are well-defined, allowing clear and accurate display of error information compared to error codes, including stack call information, aiding in better bug localization.
- The traditional error code return method has a significant issue: if a deep function returns an error, we must propagate it layer by layer for the outermost layer to receive it.
- Many third-party libraries include exceptions, such as boost, gtest, and gmock, so using them requires exception handling.
- Some functions are better handled with exceptions, like constructors without return values, making error code methods inconvenient. Functions like
T& operator[]can only use exceptions or program termination ifposis out of bounds, as returning a value cannot indicate an error.
Disadvantages:
- Exceptions cause program execution flow jumps, leading to confusion, especially when runtime errors cause unexpected jumps, making tracking, debugging, and analysis difficult.
- Exceptions incur some performance overhead, though with modern hardware speeds, this impact is generally negligible.
- C++ lacks garbage collection, requiring manual resource management. Exceptions can easily lead to memory leaks, deadlocks, and other safety issues, necessitating RAII for resource management, which has a higher learning curve.
- The C++ stendard library exception system is poorly defined, leading to custom exception systems and significant混乱.
- Exceptions should be used规范ly; otherwise, the consequences are不堪设想. Arbitrarily throwing exceptions makes it苦不堪言 for outer catchers. Exception specifications have two points: First, all thrown exception types should inherit from a common base class. Second, whether a function throws exceptions and what exceptions it throws should be规范ized using
func() throw();.