Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 5

Lesson 15.1 showed that an exception unwinds the stack and destroys locals. That says what the language does. It says nothing about what your object looks like afterwards.

A function makes one of four promises. Knowing which one you are making — and which one the functions you call make — is what separates code that survives an exception from code that merely compiles.

The four levels

| Guarantee | Promise | |---|---| | No-throw | This function will not throw. Ever. | | Strong | If it throws, nothing changed — as if it was never called. | | Basic | If it throws, everything is in a valid state, possibly a different one. No leaks. | | None | Anything may happen. Invariants may be broken, resources leaked. |

They are ordered strongest to weakest, and each is harder to provide than the one below.

"None" is not an option. A function offering no guarantee cannot be used safely in a program that uses exceptions at all, because a caller cannot know whether continuing is defined. Every function you write should offer at least the basic guarantee.

Basic versus strong, demonstrated

#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

// A type whose copy constructor can be made to throw, so the guarantees
// can be demonstrated rather than described.
struct Fragile {
    std::string name;
    static int throw_on_copy_number;
    static int copies_made;

    explicit Fragile(std::string n) : name(std::move(n)) {}

    Fragile(const Fragile& other) : name(other.name) {
        ++copies_made;
        if (copies_made == throw_on_copy_number) {
            throw std::runtime_error("copy #" + std::to_string(copies_made) + " failed");
        }
    }

    Fragile& operator=(const Fragile&) = default;
};

int Fragile::throw_on_copy_number = 0;
int Fragile::copies_made = 0;

// NO GUARANTEE: on a throw, the object is left half-updated.
class Broken {
    std::vector<Fragile> items;
    int count = 0;

public:
    void add_two(const Fragile& a, const Fragile& b) {
        count += 2;              // optimistic: record the result before achieving it
        items.push_back(a);
        items.push_back(b);      // if this throws, count is already wrong
    }

    std::size_t size() const { return items.size(); }
    int reported_count() const { return count; }
};

// STRONG GUARANTEE: build a complete new state, then swap it in.
class Safe {
    std::vector<Fragile> items;
    int count = 0;

public:
    void add_two(const Fragile& a, const Fragile& b) {
        std::vector<Fragile> candidate = items;   // may throw — nothing changed yet
        candidate.push_back(a);                   // may throw — nothing changed yet
        candidate.push_back(b);                   // may throw — nothing changed yet

        items.swap(candidate);                    // cannot throw
        count += 2;                               // cannot throw
    }

    std::size_t size() const { return items.size(); }
    int reported_count() const { return count; }
};

int main() {
    Fragile a("a");
    Fragile b("b");

    std::cout << "no guarantee:\n";
    Broken broken;
    Fragile::copies_made = 0;
    Fragile::throw_on_copy_number = 2;     // the second copy throws
    try {
        broken.add_two(a, b);
    } catch (const std::exception& e) {
        std::cout << "  caught: " << e.what() << "\n";
    }
    std::cout << "  items " << broken.size()
              << ", count says " << broken.reported_count()
              << "  <-- invariant broken\n";

    std::cout << "strong guarantee:\n";
    Safe safe;
    Fragile::copies_made = 0;
    Fragile::throw_on_copy_number = 2;
    try {
        safe.add_two(a, b);
    } catch (const std::exception& e) {
        std::cout << "  caught: " << e.what() << "\n";
    }
    std::cout << "  items " << safe.size()
              << ", count says " << safe.reported_count()
              << "  <-- unchanged\n";
    return 0;
}
no guarantee:
  caught: copy #2 failed
  items 1, count says 2  <-- invariant broken
strong guarantee:
  caught: copy #2 failed
  items 0, count says 0  <-- unchanged

Fragile is rigged so that its second copy throws, which lets the two versions be compared under identical conditions.

Broken left count at 2 with only 1 item stored. It updated the counter before doing the work that could fail. The object is now lying about itself: every future operation that trusts count is working from a false premise. That is the "none" level — the invariant count == items.size() is broken, and no caller can repair it.

Safe is untouched — 0 items, count 0. It did all the work that could throw on a copy, and only then committed the result with operations that cannot throw. The caller can retry, or give up, or carry on; the object is exactly as it was.

The technique: copy, then swap

Safe::add_two is the general recipe for the strong guarantee:

// 1. Do all the work that might throw, on a COPY.
// 2. Commit with operations that CANNOT throw — swap, move, pointer assignment.
//
// std::vector<T> candidate = items;   // may throw
// candidate.push_back(a);             // may throw
// candidate.push_back(b);             // may throw
// items.swap(candidate);              // cannot throw  <-- the commit point
// count += 2;                         // cannot throw

Everything before the commit point is reversible because nothing has changed yet. Everything after it cannot fail. There is no window in which the object is half-updated.

You have already met this idea. Copy-and-swap (lesson 9.8) is exactly this recipe applied to assignment, which is why operator= written that way is strongly exception-safe and needs no self-assignment check.

The cost is a full copy. For a large container that is real, and it is why std::vector::push_back offers the strong guarantee only when the element type's move constructor is noexcept — otherwise it must copy rather than move during reallocation, which is lesson 15.3's subject.

What the standard library guarantees

Worth knowing, because it decides what your functions can promise:

| Operation | Guarantee | |---|---| | vector::push_back, insert | strong | | vector::pop_back, clear, erase | no-throw | | vector::operator[], size, empty | no-throw | | vector::at | strong (throws out_of_range, changes nothing) | | map::insert, set::insert | strong | | map::erase | no-throw | | std::swap on containers | no-throw | | std::sort | basic — the range is valid but the order is unspecified | | std::string operations | basic; most are strong |

Two rows carry most of the weight.

Swap never throws, which is what makes copy-and-swap possible at all. If swapping could fail, there would be no safe commit point.

std::sort offers only the basic guarantee. If your comparator throws halfway through, the range still contains all the original elements — none lost, none duplicated — but in an unspecified order. Sorting is not something you can retry and expect the input back.

Writing for the basic guarantee

Most functions should aim for basic, not strong. The strong guarantee costs a copy, and for many operations it is not worth it — or not achievable.

The basic guarantee mostly comes free, provided you follow two rules:

// 1. Use RAII for every resource (lesson 15.4). No raw new, no manual close.
// 2. Do not leave an invariant broken across an operation that can throw.
//
// Broken:
//   size_ += n;              // invariant now false
//   data_ = allocate(size_); // if this throws, size_ lies
//
// Basic:
//   T* fresh = allocate(size_ + n);   // may throw — nothing changed
//   delete[] data_;                   // cannot throw
//   data_ = fresh;                    // cannot throw
//   size_ += n;                       // cannot throw

That second version is IntVector::grow_to from lesson 11.6, and the reason it allocates before freeing is precisely this.

When the strong guarantee is impossible

Sometimes you cannot offer it, and pretending otherwise is worse than admitting it:

Two independent resources. Updating two databases, or writing two files, cannot be made atomic by any amount of C++. If the second fails after the first succeeded, the first has already happened. This needs a transaction at a different layer, or a documented compensating action.

Observable side effects. A function that has already printed output, sent a packet, or notified a listener cannot un-do those. The strong guarantee is about your object's state, and it stops at the boundary of things you own.

Move-only members that cannot be copied. The copy-and-swap recipe needs a copy. When the type has none, you are limited to the basic guarantee unless the moves themselves are noexcept.

Document what you actually offer. A comment saying "basic guarantee: on failure the cache may be partially populated but remains valid" is worth more than an unmet promise of strong.

Try it

Write a Registry class holding a std::vector<std::string> of names and a std::map from name to index. Implement add(name) twice — once with the basic guarantee and once with the strong guarantee — and show what each leaves behind when the map insertion throws.

Checkpoint

  • Four guarantees: no-throw, strong, basic, and none. Never ship "none".
  • Strong means a failed call changes nothing; basic means the object stays valid.
  • Basic is satisfied by an object that is valid but wrong — valid is not the same as correct.
  • The strong guarantee is bought by copy, then commit with operations that cannot throw.
  • Copy-and-swap (lesson 9.8) is that recipe applied to assignment.
  • The commit point works only because swap never throws.
  • vector::push_back is strong; std::sort is only basic — a throwing comparator scrambles order.
  • For the basic guarantee: use RAII everywhere, and never leave an invariant broken across a throw.
  • Allocate before freeing, so a failed allocation leaves the old state intact.
  • The strong guarantee is impossible across two independent resources or observable side effects.
  • Validating before mutating removes most strong-guarantee problems for free.
  • Needing copy-and-swap often means the class holds two members that should be one.

Questions about this lesson

Log in to ask a question.