Exception safety is a crucial concept in modern C++ programming that ensures robust and reliable code in the face of unexpected errors. It refers to the ability of a program to handle exceptions gracefully, maintaining program invariants and preventing resource leaks. In this exploration, we'll look at different levels of exception safety and how to implement them in C++.
#include <iostream>
#include <stdexcept>
#include <vector>
class ResourceManager {
private:
std::vector<int> data;
public:
void addData(int value) noexcept {
try {
data.push_back(value);
} catch (const std::bad_alloc&) {
// Handle allocation failure
std::cerr << "Memory allocation failed. Data not added." << std::endl;
}
}
void displayData() const noexcept {
for (const auto& item : data) {
std::cout << item << " ";
}
std::cout << std::endl;
}
};
int main() {
ResourceManager rm;
rm.addData(10);
rm.addData(20);
rm.addData(30);
rm.displayData();
return 0;
}
This example demonstrates basic exception safety, also known as the no-throw guarantee:
addData
function is marked noexcept
, indicating that it won't throw exceptions.addData
, we catch potential std::bad_alloc
exceptions that might occur during vector::push_back
.displayData
function is also marked noexcept
as it doesn't perform any operations that could throw.#include <iostream>
#include <vector>
#include <stdexcept>
#include <algorithm>
class Database {
private:
std::vector<int> data;
public:
void addBatch(const std::vector<int>& newData) {
auto oldSize = data.size();
try {
data.insert(data.end(), newData.begin(), newData.end());
} catch (...) {
// Rollback on any exception
data.resize(oldSize);
throw; // Re-throw the caught exception
}
}
void display() const {
for (const auto& item : data) {
std::cout << item << " ";
}
std::cout << std::endl;
}
};
int main() {
Database db;
std::vector<int> batch1 = {1, 2, 3};
std::vector<int> batch2 = {4, 5, 6};
try {
db.addBatch(batch1);
db.addBatch(batch2);
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
db.display();
return 0;
}
This example illustrates strong exception safety:
addBatch
function demonstrates the commit-or-rollback semantics.display
function remains simple and doesn't throw exceptions.#include <iostream>
#include <memory>
#include <stdexcept>
class Resource {
public:
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
void use() { std::cout << "Resource used" << std::endl; }
};
class ResourceUser {
private:
std::unique_ptr<Resource> resource;
public:
ResourceUser() : resource(std::make_unique<Resource>()) {}
void performOperation() {
resource->use();
// Simulating an operation that might throw
throw std::runtime_error("Operation failed");
}
};
int main() {
try {
ResourceUser user;
user.performOperation();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
This example showcases basic exception safety using RAII (Resource Acquisition Is Initialization):
Resource
class represents a resource that needs proper cleanup.ResourceUser
employs std::unique_ptr
to manage the Resource
object, ensuring automatic cleanup.performOperation
throws an exception, the Resource
is properly released due to RAII.#include <iostream>
#include <stdexcept>
#include <memory>
class SubComponent {
public:
SubComponent(int id) {
if (id < 0) throw std::invalid_argument("Invalid ID");
std::cout << "SubComponent " << id << " created" << std::endl;
}
~SubComponent() { std::cout << "SubComponent destroyed" << std::endl; }
};
class MainComponent {
private:
std::unique_ptr<SubComponent> sub1;
std::unique_ptr<SubComponent> sub2;
public:
MainComponent(int id1, int id2)
: sub1(std::make_unique<SubComponent>(id1)),
sub2(std::make_unique<SubComponent>(id2)) {
std::cout << "MainComponent created" << std::endl;
}
~MainComponent() { std::cout << "MainComponent destroyed" << std::endl; }
};
int main() {
try {
MainComponent comp(1, -2);
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
This example demonstrates exception safety in constructors:
SubComponent
throws an exception if initialized with an invalid ID.MainComponent
uses std::unique_ptr
for exception-safe management of SubComponent
objects.sub2
, sub1
is automatically cleaned up.Exception safety is a critical aspect of writing robust C++ code. The examples provided illustrate different levels and techniques of exception safety:
Basic Exception Safety (No-throw guarantee): Ensures that operations complete without throwing exceptions, handling potential errors internally.
Strong Exception Safety: Implements commit-or-rollback semantics, ensuring that operations either complete fully or leave the object in its original state.
RAII (Resource Acquisition Is Initialization): Uses smart pointers and RAII principles to manage resources, ensuring proper cleanup even in the face of exceptions.
Exception Safety in Constructors: Demonstrates how to handle exceptions during object construction, preventing resource leaks and maintaining invariants.
By applying these techniques, C++ programmers can create more reliable and robust software that gracefully handles unexpected situations. Remember, the goal of exception safety is not just to catch exceptions, but to ensure that your program remains in a consistent state and continues to function correctly even when errors occur.