Exception handling is a crucial mechanism in C++ for dealing with runtime errors and unexpected situations that can disrupt the normal flow of a program. It allows you to gracefully handle these errors, prevent crashes, and potentially recover from them.
C++ provides the try
, catch
, and throw
keywords for exception handling:
try
: A block of code where exceptions might occur is enclosed in a try
block.throw
: When an exceptional condition is detected within the try
block (or in a function called from within the try
block), an exception is "thrown" using the throw
keyword. The operand of throw
is the exception object itself (which can be of any type).catch
: One or more catch
blocks follow the try
block. Each catch
block specifies the type of exception it can handle. When an exception is thrown, the program looks for a catch
block that can handle an exception of that type (or a compatible type).#include <iostream>
#include <stdexcept>
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero!");
}
return a / b;
}
int main() {
int x = 10;
int y = 0;
int result;
try {
result = divide(x, y);
std::cout << "Result of division: " << result << std::endl;
} catch (const std::runtime_error& error) {
std::cerr << "Caught an exception: " << error.what() << std::endl;
}
std::cout << "Program continues after exception handling." << std::endl;
return 0;
}
If an exception is thrown within a function and is not caught by a catch
block within that function, the exception is propagated back to the caller function. This process continues up the call stack until a suitable catch
handler is found. If no handler is found all the way up to the main
function, the program will typically terminate (often by calling std::terminate
).
#include <iostream>
#include <stdexcept>
void innerFunction() {
std::cout << "Inner function started." << std::endl;
throw std::runtime_error("Exception from inner function!");
std::cout << "Inner function finished." << std::endl; // This line will not be reached
}
void middleFunction() {
std::cout << "Middle function started." << std::endl;
innerFunction();
std::cout << "Middle function finished." << std::endl; // This line will not be reached
}
int main() {
std::cout << "Main function started." << std::endl;
try {
middleFunction();
} catch (const std::runtime_error& error) {
std::cerr << "Caught in main: " << error.what() << std::endl;
}
std::cout << "Main function finished." << std::endl;
return 0;
}
When an exception is thrown and propagates up the call stack, the process of destroying all local objects that were created on the stack between the point where the exception was thrown and the point where it is caught is called stack unwinding. During stack unwinding, the destructors of these local objects are automatically called, which is crucial for resource management (e.g., ensuring that dynamically allocated memory is freed).
Exception safety refers to writing code that behaves correctly even in the presence of exceptions. There are different levels of exception safety:
noexcept
specifier (introduced in C++11).noexcept
Specifier (C++11+)The noexcept
specifier can be added to function declarations to indicate whether a function is guaranteed not to throw any exceptions. This can help the compiler perform optimizations and can also affect the behavior of stack unwinding.
#include <iostream>
#include <stdexcept>
#include <vector>
void mightThrow() {
throw std::runtime_error("This function might throw.");
}
void doesNotThrow() noexcept {
std::cout << "This function does not throw." << std::endl;
}
int main() {
try {
mightThrow();
} catch (const std::runtime_error& e) {
std::cerr << "Caught exception from mightThrow: " << e.what() << std::endl;
}
doesNotThrow();
// std::vector<int> v;
// v.push_back(5); // push_back might throw if allocation fails, but std::vector tries to provide strong exception safety
return 0;
}
The Resource Acquisition Is Initialization (RAII) principle is particularly important in the context of exception handling. By managing resources (like memory, file handles, locks) through objects whose lifetime is tied to the scope, RAII ensures that resources are automatically released when an exception causes stack unwinding. Smart pointers are a prime example of RAII that helps with exception safety for dynamically allocated memory.
#include <iostream>
#include <fstream>
#include <memory>
#include <stdexcept>
class FileWrapper {
private:
std::ofstream file;
std::string filename;
public:
FileWrapper(const std::string& fname) : filename(fname), file(fname) {
if (!file.is_open()) {
throw std::runtime_error("Could not open file: " + filename);
}
std::cout << "File '" << filename << "' opened." << std::endl;
}
~FileWrapper() {
if (file.is_open()) {
file.close();
std::cout << "File '" << filename << "' closed." << std::endl;
}
}
void write(const std::string& data) {
if (file.is_open()) {
file << data << std::endl;
} else {
throw std::runtime_error("File not open for writing.");
}
}
};
void processFile() {
try {
FileWrapper writer("output_exception.txt");
writer.write("First line.");
writer.write("Second line.");
// Simulate an exception
if (true) {
throw std::runtime_error("Simulated error during processing.");
}
writer.write("This line might not be written.");
} catch (const std::runtime_error& e) {
std::cerr << "Exception caught in processFile: " << e.what() << std::endl;
}
// The FileWrapper object 'writer' will be destroyed when it goes out of scope,
// automatically closing the file, even if an exception occurred.
}
int main() {
processFile();
return 0;
}
You can define your own exception classes by inheriting from the standard std::exception
class or one of its derived classes (like std::runtime_error
, std::logic_error
, etc.). This allows you to provide more specific information about the errors that can occur in your program.
#include <iostream>
#include <exception>
#include <string>
class MyCustomError : public std::runtime_error {
public:
MyCustomError(const std::string& message) : std::runtime_error(message) {}
};
void doSomething(int value) {
if (value < 0) {
throw MyCustomError("Value cannot be negative: " + std::to_string(value));
}
std::cout << "Processing value: " << value << std::endl;
}
int main() {
try {
doSomething(10);
doSomething(-5);
} catch (const MyCustomError& error) {
std::cerr << "Caught custom error: " << error.what() << std::endl;
} catch (const std::runtime_error& error) {
std::cerr << "Caught a runtime error: " << error.what() << std::endl;
} catch (const std::exception& error) {
std::cerr << "Caught a standard exception: " << error.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception." << std::endl;
}
return 0;
}
Effective exception handling is essential for writing robust and reliable C++ programs. By using try
, catch
, and throw
, ensuring exception safety, and defining custom exceptions, you can manage errors gracefully and maintain program stability.