CS247 Lecture 13

Last Time: Polymorphic assignment, exceptions This Time: Exception safety, RAII, smart pointers.

We saw in the last lecture, we can throw different type of exceptions from catch blocks. We can also re-throw the same exception to deal with:

try {
	...
} catch(std::exception& e){
	... 
	throw;
}

Why here do I just say throw rather than throw e?

  • “throw e” performs a copy in order to throw this exception
  • Remember that copy constructors (any type of constructor) cannot be virtual. Therefore, the static type is used for the copy.

If you throw a range_error and catch via std::exception& catch block,

  • throw re-throws range_error.
  • throw e catch a std::exception copied from the range_error, we lose the dynamic type.

Summary

In the previous catch block, we are catching exceptions of type std::exception by reference, which means it can catch expressions of std::exception type and any of its derived classes. If an exception of type range_erro (derived from std::exception) is thrown in the try block, it will catch it.

throw vs. throw e

  1. Using throw without specifying the exception object, we are essentially re-throwing the currently caught exception. The dynamic type of the exception is preserved, meaning that the original type of the expression that was thrown is still recognized and can be caught by a matching catch block further up the call stack. Allows the program to handle the specific exception type with the appropriate catch block.
  2. Using throw e: we are creating a new exception of the same type as e (which is of type std::exception) and throwing that new exception (we don’t have specific type anymore!). While the exception is derived from the base std::exception type, the dynamic type information is lost because we constructed a new object. As a result, only the std::exception part of the exception is considered, and we lose access to any additional behaviour or information provided by the derived exception type, such as range_error. Can lead to incorrect or incomplete error handling.

Generally, catch blocks should catch by reference to avoid copies.

Never let a destructor throw an Exception Handling!

Default behavior: Program immediately craches. That is if an exception is thrown during the destruction of an object and not caught within the destructor itself, the program terminates abruptly. This behavior is known as stack unwinding.

noexcept is a tag associated with methods (like const), which states the method will never raise an exception.

By default, destructors are implicitly tagged with noexcept, meaning that they are expected not to throw any exceptions. That is throwing an exception during stack unwinding can lead to multiple exception being active at the same time, making program’s state unpredictable and hard to handle correctly.

We can allow exceptions thrown from destructors by tagging them noexcept (false). This can happen when you want to provide more information about an exceptional condition occurring during the destruction process or when you have specific error handle mechanism in place to handle such exceptions.

For example:

class A {
	public:
		...
		~ A() noexcept (false) {
			...
			throw SomeException(); // Explicitly allowing exceptions to be thrown
		}
};

Danger

If we throw an exception, stack unwinding occurs, during this process destructors are running for stack allocated objects. If one of these destructors throws, now we have 2 active exceptions!

  • 2 active exceptions = program crash (this behavior cannot be changed)

Exception Safety

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 fo sequential execution.

Let’s fix f, so we can handle its memory leak.

void f() {
	MyClass m;
	MyClass* p = new MyClass{};
	try {
		g();
	} catch (...) {
		delete p;
		throw;
	}
	delete p;
}

Complaints about the fix above:

  1. Repeated code. delete p twice, a little annoying.
  2. Could get complicated for many pointers, many function calls, etc.

In other languages, some have “finally” clause, which always runs, either after a successful try, a successful catch, or before a catch returns. Unfortunately, C++ doesn’t have support for this.

  • The only guarantee is that during stack unwinding, stack allocated objects have their destructors run.
  • Therefore, use stack allocated objects instead no leak

What if we need a pointer? For example, to achieve Polymorphism

One solution: wrap the pointer in a stack allocated object that will delete it for us during stack unwinding. ???????? How do I do that??????? (Smart pointers)

C++ Idiom: RAII (resource acquisition is initialization)

Example:

{
	ifstream file{"file.txt"};
	...
}

Resource (file handler) is acquired in the constructor (initialization). Resource is freed (closing the file) in the destructor.

Apply this RAII concept to dynamic memory. std::unique_ptr<T> (in <memory> library)

  • contains a T* passed via the constructor
  • deletes the T* in the destructor
void f() {
	MyClass m:
	std::unique_ptr<MyClass> p{new MyClass{}};
	g();
}

If we leave f normally, or if we leave f during stack unwinding, either way, p’s destructor runs, deletes heap memory (because of implement of unique_ptr).

In between, we can use p like a regular pointer thanks to operator overload. ????? Why thanks to operator overload?????? A: It’s just that we can use p as a regular pointer thanks to the fact that std::unique_ptr overloads operators such as -> and * to mimic pointer behaviour.

Generally, we don not call the constructor directly with a pointer, because of the following issues:

  1. std::unique_ptr<T> p{newT{}}; new is not paired with a delete (not one that we see, at least) If an exception is thrown in this scenario after the new but before the std::unique_ptr is constructed, the dynamically allocated memory might not get properly deallocated, leading to memory leak. We can avoid this by directly initializing the std::unique_ptr with a new-allocated object. (Initialize a std::unique_ptr using std::make_unique):
std::unique_ptr<T> p = std::make_unique<T>(); // Using std::make_unique
// or
auto p = std::make_unique<T>(); // Using auto and std::make_unique
  1. Causes a double delete:
T* p = new T{};
std::unique_ptr<T> w{p};
std::unique_ptr<T> r{p}; // Will lead to double delete

If we use raw pointer to create an object and then use that pointer to initialize more than one std::unique_ptr, we’ll end up with a double deletion problem because each std::unique_ptr will try to delete the same memory. So it is unsafe.

  1. g() can potentially throw in the example below, heap-allocated objects does not get deleted
f(std::unique_ptr<T> {new T()}, g());

If g() throws an exception, the std::unique_ptr created with new T() would be lost, and the memory allocated by new would not be properly deallocated, leading to memory leak. This can be avoided by using std::make_unique or manually handling exceptions and memory cleanup. This is because of the order of operations and exception handling. If g() throws an exception:

  • If g() throws an exception before the temporary std::unique_ptr<T> object is constructed, then no memory leak will occur because the memory allocated by new T() will not be lost; it has never been assigned to a std::unique_ptr yet.
  • If g() throws an exception after the temporary std::unique_ptr<T> object is constructed but before f is called, then the std::unique_ptr<T> object will go out of scope and properly delete the dynamically allocated memory (because its destructor will be called during the stack unwinding caused by the exception).
  • However, if g() throws an exception after the f function is called and the temporary std::unique_ptr<T> object is passed to f, then the dynamically allocated memory will be leaked because the exception prevents proper cleanup. the std::unique_ptr<T> object’s destructor will not be called because the exception unwinds the stack. The temporary std::unique_ptr<T> object’s destructor will not be called automatically because it goes out of scope when the full expression is evaluated (after g() is called), not at the exact point where g() throws. Since we throw when g() is called, we will never get to finish and call the destructor.

Summary

If we use new to allocate memory separately and then assign the raw pointer to a std::unique_ptr, you could encounter memory leaks or double-deletion issues if the code is not designed carefully. Directly initializing a std::unique_ptr using its constructor or using std::make_unique is recommended

One potential ordering (in C++14) (obscure scenario)

  1. new T()
  2. g()
  3. unique_ptr constructor
  4. f()

Preferred alternative: std::make_unique<T>(…)

  • Constructs a T object in the heap with arguments …, and returns a unique_ptr<T> pointing at this object.

Something else to consider:

unique_ptr<T> p = make_unique<T>(...);
unique_ptr<T> q = p;

Call copy constructor to copy p into q.

What happens? Doesn’t compile. Copying is disabled for unique_ptrs. They can only be moved. (Compile time error if it does happen)

This is achieved by setting copy constructor / assignment operator to = delete. C++ ensures that we cannot copy std::unique_ptr instances by implementing = delete (which is a trick we can also use). Only able to move them, allowing proper ownership transfer instead.

unique_ptr are good for representing ownership, since when one object dies, its unique_ptr fields will run and clean up the associated object.

Has-a relationship: Use raw pointers or references.

  • You can access the underlying raw pointer of a smart pointer via .get().

Note

The .get() method is a member function provided by C++ smart pointer classes, such as std::unique_ptr, std::shared_ptr, etc. This method allows you to retrieve the raw pointer that the smart pointer is managing. In other words, .get() returns the actual pointer value stored by the smart pointer.

Next: CS247 Lecture 14