๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

C++ Exception Safety Guarantees

Exception safety is how code stays correct when an operation throws an exception. Three named guarantees describe how much of the state survives a failure.

Validating first keeps the balance

The setter checks both new values before it changes anything. The refused call throws, and the balance stays at 100.

#include <iostream>
#include <stdexcept>

class Wallet {
public:
  void setBoth(int newBalance, int newLimit) {
    if (newBalance < 0 || newLimit < 0) {
      throw std::runtime_error("Negative value refused");
    }
    balance_ = newBalance;
    limit_ = newLimit;
  }
  int balance() const {
    return balance_;
  }
  int limit() const {
    return limit_;
  }
private:
  int balance_ = 100;
  int limit_ = 50;
};

int main() {
  Wallet wallet;
  try {
    wallet.setBoth(-5, 80);
  } catch (const std::runtime_error& error) {
    std::cout << error.what() << "\n";
  }
  std::cout << "Balance: " << wallet.balance() << "\n";
  return 0;
}

Changing first leaves a partial change

This setter stores the new balance before it checks the limit. The throw arrives after the first change, and the balance shows the damage.

#include <iostream>
#include <stdexcept>

class Wallet {
public:
  void setBoth(int newBalance, int newLimit) {
    balance_ = newBalance;
    if (newLimit < 0) {
      throw std::runtime_error("Negative value refused");
    }
    limit_ = newLimit;
  }
  int balance() const {
    return balance_;
  }
  int limit() const {
    return limit_;
  }
private:
  int balance_ = 100;
  int limit_ = 50;
};

int main() {
  Wallet wallet;
  try {
    wallet.setBoth(200, -1);
  } catch (const std::runtime_error& error) {
    std::cout << error.what() << "\n";
  }
  std::cout << "Balance: " << wallet.balance() << "\n";
  return 0;
}

Copy and swap builds the new state first

The replace function builds the new record in a temporary before it swaps anything. The build throws, the swap never runs, and the old record stays whole.

#include <iostream>
#include <stdexcept>
#include <utility>

class Record {
public:
  Record(int low, int high) {
    if (low < 0 || high < 0) {
      throw std::runtime_error("Negative value refused");
    }
    low_ = low;
    high_ = high;
  }
  void replace(int newLow, int newHigh) {
    Record next(newLow, newHigh);
    std::swap(low_, next.low_);
    std::swap(high_, next.high_);
  }
  int low() const {
    return low_;
  }
  int high() const {
    return high_;
  }
private:
  int low_;
  int high_;
};

int main() {
  Record record(10, 20);
  try {
    record.replace(-1, 99);
  } catch (const std::runtime_error& error) {
    std::cout << error.what() << "\n";
  }
  std::cout << "Low: " << record.low() << "\n";
  std::cout << "High: " << record.high() << "\n";
  return 0;
}

Example

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

class Profile {
public:
  Profile(const std::string& name, int score) {
    name_ = name;
    score_ = score;
  }
  void update(const std::string& newName, int newScore) {
    if (newScore < 0) {
      throw std::runtime_error("Negative value refused");
    }
    name_ = newName;
    score_ = newScore;
  }
  std::string name() const {
    return name_;
  }
  int score() const {
    return score_;
  }
private:
  std::string name_;
  int score_;
};

int main() {
  Profile profile("Ana", 70);
  try {
    profile.update("Bo", -3);
  } catch (const std::runtime_error& error) {
    std::cout << error.what() << "\n";
  }
  std::cout << "Name: " << profile.name() << "\n";
  std::cout << "Score: " << profile.score() << "\n";
  profile.update("Bo", 85);
  std::cout << "Name: " << profile.name() << "\n";
  std::cout << "Score: " << profile.score() << "\n";
  return 0;
}

Key points

  • Exception safety is the set of promises code keeps when an operation fails partway.
  • The basic guarantee promises that a failed operation leaks nothing and leaves the object usable.
  • The strong guarantee promises that a failed operation leaves the state exactly as it was before the call.
  • The no-throw guarantee promises that an operation cannot fail at all.
  • Checking every new value before changing any stored value gives the strong guarantee.
  • Building the new state in a temporary and swapping it in also gives the strong guarantee.

In this example, the profile update checks the new score before it stores anything. The refused update prints the error and leaves Ana with a score of 70. The accepted update then stores both new values, and the profile prints Bo with a score of 85.

Note

The guarantees describe the state after a failure, not whether the failure can happen.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

A failed operation leaves the state exactly as it was before the call. Which guarantee is that?