Skip to Content
Course content

104: Writing Exception-Safe Container Operations

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a chef plating a high-end dish. You've got the steak perfectly seared and placed in the center of the plate. Now, you start drizzling a complex reduction sauce over it. halfway through, you realize the sauce has broken—it's a disaster. You can't serve a plate with half a sauce drizzle. You have two choices: either you can magically wipe the plate clean and pretend you never started, or you're stuck serving a ruined meal. In the culinary world, that's a waste of a steak. In C++, that's a corrupted container.

When we talk about exception safety in containers, we're really talking about what happens to your data when something goes wrong mid-operation. If you're resizing a buffer or adding an element, and a std::bad_alloc or a copy-constructor exception hits, you can't leave your container in a "half-baked" state where the size variable says 10, but only 5 elements were actually constructed.

The Disaster of the Half-Finished Update

I've seen too many developers write code like this when trying to update a container's internal buffer. It looks intuitive, but it's a minefield:

void MyContainer::update_data(const std::vector<int>& newData) {
    this->clear(); // State is now empty
    for(auto item : newData) {
        this->push_back(item); // What if this throws on the 5th element?
    }
}

If push_back throws an exception halfway through that loop, your original data is gone (because of clear()), and your new data is only partially loaded. You've just entered the "Basic Guarantee" territory—the program hasn't crashed and no memory is leaked, but your object is now functionally useless. In a production system, that's often just as bad as a crash.

The 'All-or-Nothing' Commitment

When I'm reviewing code, I always push for the Strong Exception Guarantee for container modifications. This is the "transactional" approach: the operation either succeeds completely, or the state of the program remains exactly as it was before the call. No half-finished plates.

To achieve this, you have to avoid modifying the actual state of your object until you are absolutely certain that every operation that could throw has already succeeded. You do the "scary" work on the side, and once the danger has passed, you perform a non-throwing swap.

Using Copy-and-Swap to Save Your Sanity

The most elegant way to handle this is the Copy-and-Swap idiom. Instead of modifying your member variables directly, you create a temporary copy of your data, modify that copy, and then swap the temporary's internals with your own. Swapping pointers or handles is generally a noexcept operation, meaning it cannot fail.

Here is how I would rewrite that update_data function to make it bulletproof:

void MyContainer::update_data(const std::vector<int>& newData) {
    // 1. Create a temporary container (the 'side plate')
    // If this throws, the original this->data is untouched.
    MyContainer temp(newData); 

    // 2. Swap the internals of 'this' with 'temp'
    // std::swap on vectors/pointers is guaranteed not to throw.
    std::swap(this->data, temp.data);
    
    // When 'temp' goes out of scope, it carries the OLD data 
    // away to be deleted automatically.
}

Notice the difference? If the constructor for temp throws because the system is out of memory, the function exits, the exception propagates, but your MyContainer instance is still exactly as it was before the call. You've preserved the state of the world.

One quick tip: always mark your swap functions and move constructors as noexcept. If the compiler doesn't know a swap is safe, it might avoid using it in certain optimizations, and you lose the very guarantee you're trying to build.




📋 Practical Task

Implement a Transactional Buffer Replace

You are building a custom FixedBuffer class that manages a raw heap array of integers. The class has a method replace_contents(const std::vector<int>& newValues). Currently, this method deletes the old array and allocates a new one, but it is not exception-safe; if the new allocation fails or a copy throws, the buffer is left empty and the original data is lost.

Your Task: Rewrite the replace_contents method using the Copy-and-Swap philosophy to provide the Strong Exception Guarantee. Ensure that if an exception occurs during the processing of newValues, the FixedBuffer retains its original data intact.


class FixedBuffer {
    int* data = nullptr;
    size_t size = 0;

public:
    FixedBuffer(size_t s) : size(s), data(new int[s]) {}
    ~FixedBuffer() { delete[] data; }

    // This is the method you need to fix
    void replace_contents(const std::vector<int>& newValues) {
        // TODO: Implement using a temporary object and std::swap
        // to ensure the Strong Exception Guarantee.
    }

    // Provided helper for the swap
    void swap(FixedBuffer& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
};
Rating
0 0

There are no comments for now.

to be the first to leave a comment.