Lecture 22

Last Time: CRTP, Polymorphic cloning This Time: Exception Safety, Template functions?

Recall: We saw that reasoning about programs with exceptions is tricky.

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 program’s performance.
  2. Debugging and Maintenance: Memory leaks can be indicative of other code problems.
  3. Reliability and Robustness: Repeated crashes and memory leaks in 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 in C++ (std::unique_ptr, std::shared_ptr), which manage memory automatically even when exceptions occur.

We can be more specific about how safe our program is given an exception is thrown.

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.
class A{...};
class B{...};
 
class C{
	A a;
	B b;
	public:
		void f(){
			a.g();
			b.h();
		}
};

What is the exception safety of f?

A::g and B::g both provide the string guarantee. f - only satisfies the basic guarantee 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. ???????? (what does it mean 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, “pointer to implementation” idiom

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

Dereferencing pImpl gives us a CImpl&. Then we call make_unique<CImpl> with this CImpl&. make_unique creates an entirely new unique pointer for us, and it will invoke the copy constructor for CImpl (because make_unique takes a CImpl’s constructors arguments, and the constructor that takes in a const CImpl& is the copy constructor).

Remember for unique_ptr

The copy ctor and copy assignment operator of unique_ptr is disabled by the standard library.

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.

Note

In this version of C::f(), you create a temporary CImpl object (temp) and perform all modifications on it. This ensures that no changes are made to the actual C object if an exception occurs. The std::swap operation at the end is guaranteed not to throw exceptions, and it swaps the contents of temp and pImpl, effectively committing the changes only if everything succeeds without exceptions.

This code achieves the strong exception safety guarantee because if a.g() or b.h() throws, the C object remains unchanged, and if the swap operation throws, it’s done after the modifications are isolated in the temporary object temp.

PImpl Idiom can also be used in some scenarios for reducing compilation dependencies.

typically: C.h

class C {
	A a; // If any of the private fields change, everything has to recompile
	B b;
};

On the other hand: CImpl.h

struct CImpl {
	A a;
	B b;
};

C.h

class CImpl;
class C {
	unique_ptr<CImpl> pImpl;
	...
};

Now, if cImpl changes only C.cc must recompile. Because we only use a forward declaration in C.h no recompilation.

Explanation

With the PImpl idiom, you encapsulate the private implementation details in a separate structure (CImpl) that’s defined in its own header file (CImpl.h). You only forward declare CImpl in C.h, which means you’re not exposing the actual implementation details to the code including C.h.

Finally - we could make CImpl a polymorphic class - swap out implementation at runtime - the Bridge Pattern.

Back to Exception Safety - vector::emplace_back. Gives us the strong guaranteed. How?

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

  • Allocate a new, larger array.
  • Invoke copy constructor 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 wanted 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.??? Wrong right

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.

Next: CS247 Lecture 23