[2026] C++ Exception Safety — Basic, Strong, and Nothrow Guarantees

[2026] C++ Exception Safety — Basic, Strong, and Nothrow Guarantees

이 글의 핵심

A guide to C++ exception safety: what guarantees mean, how they relate to RAII and noexcept, and practical patterns (copy-and-swap, safe updates) with code.

What is exception safety?

Exception safety is about which program state you preserve when an exception is thrown—especially no resource leaks and consistent object invariants.

Example implementations of func:

// ❌ Not exception-safe
void func() {
    int* ptr = new int(10);
    process();  // leak if this throws
    delete ptr;
}

// ✅ Exception-safe
void func() {
    auto ptr = std::make_unique<int>(10);
    process();  // unique_ptr cleans up on any exit path
}

Levels of guarantee

// 1. Basic guarantee
// - No resource leaks
// - Object remains in a valid (though possibly altered) state; invariants hold

// 2. Strong guarantee
// - Operation succeeds fully, or the observable state is unchanged
// - Often described as "commit or rollback" / atomic effect on state

// 3. Nothrow guarantee
// - No exceptions propagate from this operation
// - Often expressed with noexcept

Practical examples

Example 1: Basic guarantee

class Buffer {
    int* data;
    size_t size;

public:
    void resize(size_t newSize) {
        int* newData = new int[newSize];  // may throw

        size_t copySize = std::min(size, newSize);
        std::copy(data, data + copySize, newData);

        delete[] data;
        data = newData;
        size = newSize;
    }
};

Example 2: Strong guarantee (manual cleanup on copy failure)

class Buffer {
    int* data;
    size_t size;

public:
    void resize(size_t newSize) {
        int* newData = new int[newSize];

        try {
            std::copy(data, data + std::min(size, newSize), newData);
        } catch (...) {
            delete[] newData;  // clean up partial work
            throw;             // rethrow
        }

        delete[] data;
        data = newData;
        size = newSize;
    }
};

Example 3: Nothrow guarantee

class Widget {
public:
    void swap(Widget& other) noexcept {
        std::swap(data, other.data);
    }

    Widget(Widget&& other) noexcept
        : data(other.data) {
        other.data = nullptr;
    }

private:
    int* data;
};

Example 4: copy-and-swap

class Buffer {
    int* data;
    size_t size;

public:
    Buffer& operator=(const Buffer& other) {
        Buffer temp(other);  // copy; may throw
        swap(temp);          // nothrow
        return *this;
    }

    void swap(Buffer& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
};

Using RAII

class Transaction {
public:
    Transaction() {
        begin();
    }

    ~Transaction() {
        if (!committed) {
            rollback();  // rollback if stack unwinds before commit
        }
    }

    void commit() {
        // ...
        committed = true;
    }

private:
    bool committed = false;
    void begin() {}
    void rollback() noexcept {}
};

Common pitfalls

Pitfall 1: Partial updates

Example update implementations:

// ❌ Partial update, then exception
void update(Data& d) {
    d.x = 10;
    d.y = compute();  // if this throws, x changed but y/z did not
    d.z = 30;
}

// ✅ Staging in a temporary, then assign
void update(Data& d) {
    Data temp = d;
    temp.x = 10;
    temp.y = compute();
    temp.z = 30;
    d = temp;  // strong guarantee if assignment is basic-safe and nothrow swap
}

Pitfall 2: Resource leaks

// ❌ Manual ownership
void func() {
    Resource* r1 = new Resource();
    Resource* r2 = new Resource();  // if this throws, r1 leaks

    delete r1;
    delete r2;
}

// ✅ RAII
void func() {
    auto r1 = std::make_unique<Resource>();
    auto r2 = std::make_unique<Resource>();
}

Pitfall 3: Exceptions in destructors

// ❌ Throwing destructor
class Bad {
public:
    ~Bad() {
        throw std::runtime_error("error");  // undefined behavior if unwinding
    }
};

// ✅ non-throwing destructor
class Good {
public:
    ~Good() noexcept {
        try {
            cleanup();
        } catch (...) {
            // swallow: destructor must not escape
        }
    }
};

Pitfall 4: Multiple raw resources

// ❌ Not exception-safe
void func() {
    Resource* r1 = new Resource();
    Resource* r2 = new Resource();  // leak if this throws after r1 allocated
}

// ✅ Sequential RAII
void func() {
    auto r1 = std::make_unique<Resource>();
    auto r2 = std::make_unique<Resource>();
}

Design principles

// 1. Prefer RAII
std::unique_ptr<Resource> resource;

// 2. Destructors should be noexcept
~MyClass() noexcept {}

// 3. swap should be noexcept
void swap(MyClass& other) noexcept {}

// 4. Move operations should be noexcept when possible
MyClass(MyClass&&) noexcept {}

FAQ

Q1: What are the exception-safety levels?

A:

  • Basic: No leaks; object stays valid (invariants hold).
  • Strong: All-or-nothing effect on observable state (often via copy-and-swap).
  • Nothrow: No exceptions escape (often noexcept).

Q2: What role does RAII play?

A: Automatic, scope-based resource management—it is the main tool for basic guarantee and leak freedom.

Q3: May destructors throw?

A: No. Destructors should not throw; use noexcept and contain errors.

Q4: How do I get the strong guarantee?

A: Common pattern: copy-and-swap for assignment-like operations.

Q5: Performance?

A: RAII wrappers like unique_ptr are typically no overhead vs correct manual delete. Throwing has cost on the error path; the non-throwing path is still “pay for what you use.”

Q6: Where can I read more?

A:

  • Effective C++
  • C++ Coding Standards
  • Exception Safety in C++ (Stroustrup / Sutter-era guidance)

Posts that connect to this topic: