Exception

Exception Safety

The basic idea is that we want our program to be exception safe. Introduced in CS247.

There are 4 levels of exception safety:

  1. No exception safety - If an exception is thrown - no guarantees about program state - object has invalid memory, memory is leaked, program crashes.
  2. Basic guarantee - if an exception is thrown - program is in a valid, but unspecified state. Ex: Class invariants are maintained, no memory is leaked.
  3. Strong guarantee - If an exception is thrown, the program reverts back to its state before the method was called. Ex: vector::emplace_back
    • Either it succeeds, or if exception is thrown, vector is in its prior state
  4. Nothrow guarantee - exceptions are never propagated outside of the function call - always does its job. Ex: vector::size gives nothrow guarantee, always returns size of vector without fail.

Motivation

Consider the following program:

void f() {
	MyClass m;
	MyClass* p = new MyClass{};
	g();
	delete p;
}

Under normal circumstances, f does not leak memory. But, if g throws, then we do not execute delete p, so we do leak memory!

Thing to recognize

Exceptions significantly change control flow! We no longer have the guarantee of sequential execution.

A fix is to use unique_ptr.

void f() {
	int* p = new int{247};
	g();
	delete p; // memory is leaked iff g throws
}

Why do we care if the program is going to crash anyways?

We care about memory leaks even when a program is going to crash due to factors like:

  1. Resource Management: The operating system might not reclaim all resources, affecting other programs’ performance.
  2. Debugging and Maintenance: Memory leaks can be indicative of other code problems.
  3. Reliability and Robustness: Repeated crashes and memory leaks in a larger system can cause system-wide issues.
  4. Graceful Failure: Even in a crash, proper resource cleanup provides better debugging information and minimizes impact on the system.

To avoid such leaks, use exception-safe practices like Smart Pointers.

Exercise

class A{...};
class B{...};
 
class C {
	A a;
	B b;
	public:
		void f() {
			a.g();
			b.h();
		};
}

What is the exception safety of f, given that A::g and B::h both provide the strong guarantee?

  • Answer: f - only satisfies the basic guarantee.

Why?

This is because if a.g() throws, then because it provides strong guarantee - it’s as if it was never called. When exception propagates from f, f has not done anything.

If b.h() throws - it provides the strong guarantee, so it does undo any of its effects. BUT: it has no knowledge that a.g() ran just prior. As a result, when exception propagates from f, the effects of a.g() remain in place.

Program is in a valid yet unspecified state basic guarantee.

Right now, there may be effects of a.g() that are impossible to undo writing to a file, printing to the screen, sending a network request.

For simplicity, we’ll assume A::g and B::h have only local side effects (i.e. only the a and b objects and any associated memory may change).

Now, how may we try to rewrite f to achieve the strong guarantee? Idea: Use Copy-and-Swap Idiom, only work on temporary objects rather than real ones.

class C {
	A a;
	B b;
	public:
		void f() {
			A aTemp{a};
			B bTemp{b};
			aTemp.g();
			bTemp.h();
			a = aTemp; // copy assignment operator (could throw)
			b = bTemp; 
		}
}

If aTemp.g() or bTemp.h() throw - no changes are made to the C object, it’s as if f was never called.

This is unfortunately, still the basic guarantee. Because, if b = bTemp throws, we propagate an exception having modified a.

What we really need for this is a non-throwing swap or assignment. Assigning a pointer will never throw. One possible solution: PImpl Idiom.

struct CImpl { // "Impl" class has the fields
	A a;
	B b;
};
 
class C {
	unique_ptr<CImpl> pImpl;
	public:
		void f() {
			unique_ptr<CImpl> temp = make_unique<CImpl>(*pImpl);
			temp->a.g();
			temp->b.h();
			std::swap(temp, pImpl); // guaranteed nothrow
		}
};

This is the strong guarantee - if a.g() or b.h() throw - all work is done on temp, so we’re fine.

Otherwise, swap will always succeed f will do its job.

vector::emplace_back. Gives us the strong guarantee. How?

Easy case: No resize, just put the object in the array. When resizing:

  • allocate a new, larger array
  • Invoke array copy constructor to copy objects of type T to new array
    • If copy constructor throws: delete the new array, old array is left intact.
  • Then delete old array, and set array pointer to new array (nothrow).

Complaint: This is a lot of copies when all I really want was to resize my array.

Better, would be to move:

  • Allocate a new array
  • Move objects from the old array to the new array
    • If a move throws, move all our objects back from new array to old array, delete new array
  • Delete old array, set pointer to new array

Problem: If move throws once, it might also when moving objects back. Once we’ve modified our old array, no guarantee we can restore it.

Solution: If move constructor is declared as “noexcept”, then emplace_back will perform moves. Otherwise, it will copy over every item, and do this try-catch block to delete old items if necessary.

If possible, moves and swaps should provide the nothrow guarantee - and you should make this explicit to the compiler via the noexcept tag.

class MyClass {
	public:
		MyClass(MyClass&& other) noexcept {...}
		MyClass operator=(MyClass&& other) noexcept {...}
};

If you know a function will never propagate an exception - declare it noexcept to facilitate optimization.

Moves and swaps at the minimum should be noexcept.

Nothrow Guarantee

Does adding the noexcept keyword automatically mean nothrow guarantee?

I thought yes, but no. It does not.

No, the noexcept keyword in C++ does not automatically mean a “nothrow” guarantee. It is a promise that a function will not emit exceptions, but if an exception is thrown inside a noexcept function, it will call std::terminate.

So nothrow guarantees are much stronger than noexcept, because noexcept can still throw.

Source

Examples of nothrow guarantees:

  • std::swap
  • vector::size