Lecture 12

Last Time: Pure virtual, polymorphic arrays, polymorphic Big 5 This Time: Polymorphic assignment problem finished, exceptions

Continuing our implementation of AbstractBook to fix both the mixed and partial assignment problem.

class AbstractBook {
	string title, author;
	int length;
	protected:
		AbstractBook& operator=(const AbstractBook& other) = default;
	public:
		AbstractBook(...) {}
		virtual ~AbstractBook() = 0;
};

To make this abstract, we need a pure virtual method. If no other methods make sense to be pure virtual, we can always use destructor.

class Text: public AbstractBook {
	string topic;
	public:
		Text(...) {...}
		// implicitly: this is implemented: Text& operator=(const Text& t);
};

How does this fix the two problem?

  1. Mixed assignment: operator= is non-virtual and the implicitly provided copy assignment operator only accepts Text.
  2. Partial assignment problem:
Text t1{...};
Text t2{...};
AbstractBook& br1 = t1;
AbstractBook& br2 = t2;
br2 = br1; //doesn't compile, AbstractBook:operator= is protected
}

Note: only works since AbstractBook is an abstract class. If we set Book::operator= to be protected then we couldn’t assign books to one another. ??????????? (What does this sentence mean)

Consider Text’s destructor. Implicitly, the following happens

  1. Destructor body runs (empty)
  2. Object fields we destructed in reverse decl. order
  3. Superclass destructor runs
  4. Space is reclaimed

Because in step 3, Text’s destructor calls AbstractBook’s destructor, we have a problem: we’ve called a method with no implementation.

Solution: give it an implementation:

AbstractBook::~AbstractBook(){}

Nnote

Pure virtual methods don’t have to be implemented, but they still can be. They require an implementation if they will be called.

AbstractBook is still abstract, only subclasses of classes which define a pure virtual method may be concrete. (Doesn’t mean that the destructor that is pure virtual has an implementation will make it concrete. It is still an abstract class)

One possible recommendation: If you care about assignment, consider making your superclasses abstract. (What does care about assignment mean??)

Error Handling

Example from STL vector

Vectors: dynamically allocated resizing arrays. Handles memory management so we don’t screw it up.

Unfortunately, we can’t idiot proof everything.

vector<int> v;
v.pusch_back(100);
cout << v[100] << endl; // Out of bounds access, likely seg fault
  • Ross complains about cs138 v.at() and v[i] being checked or unchecked.

How to handle errors: Option 1: Sentinel values. Reserve some values, -1, INT_MIN to signal errors:

  • Problem: reduces what we can return, can’t return -1 in a regular scenario. Not clear for a general type what values we should pick as sentinels.

Option 2: global variables. Create some global variable that it set when an error occurs (in C, int errno, which can be queried for errors with standard functions).

  • Also not ideal: Limited to the number of errors, might be overwritten.

Option 3: Bundle in a struct:

template <typename T> struct Return Type {
	int errorcode;
	T* data;
}
  • Best so far, but still not ideal. Wrap our return types in this struct, all return types are larger than needed. Awkward to access data field.

These are all approaches that C users end up using. C++ however has a language feature for dealing with errors: exceptions.

v.at(100) fetches v[100] if the value exists, otherwise throws an exception.

try{
	cout << v.at(100) << endl;
} catch(std::out_of_range r){
	cout << "RangeError" << r.what() << endl;
}
  • r is just an object, class type is std::out_of_range (included in <std except>)
  • The .what() method returns a string describing the exception.

Force the programmer to deal with the error because the control flow jumps. Vector knows the error happened but not how to fix it. We know how to fix it, but not how the error occurred non locality error handling.

To raise an exception ourself, we use the “throw” keyword. We can throw any value, but keep in mind that <stdexcept> has objects for common scenarios like out_of_range, logic_error, invalid_argument.

When an exception is raised, control flow steps. (control flow is interrupted). Program starts to search through the stack upwards (reverse order) looking for a handler for this type of exception “Stack unwinding”. As the control flow moves up the call stack, destructors are run for objects stored on the stack during the process of stack unwinding. (Ensures that any resources held by those objects are properly released and cleaned up). If a handler is found, we jump to that point. If no handler is found, program crashes.

call stack

A call stack is a data structure that keeps track of function calls and their respective contexts.

Summary

When an exception is thrown in C++, the control flow jumps to find an appropriate handler for the exception. If a handler is found, the program continues execution from that point. If no handler is found, the program crashes. Stack unwinding takes place during the search for a handler, executing destructors to clean up objects on the stack. Exception handling allows programmers to deal with errors and exceptional conditions in a structured and controlled manner.

Example:

void f(){
	throw std::out_of_range{"f threw"};
}
 
void q() {f();};
void h() {q();};
 
int main(){
	try {h();}
	catch(std::out_of_range r){
		cout << r.what();
	}
}

Main calls h, h calls q, q calls f, throws, stack unwinding through q, h, jump to catch block in main.

Multiple errors may be handled via multiple catch blocks:

try{...}
catch(out_of_range r) {...}
catch(logic_error e) {...}
catch(invalid_argument i) {..}
catch(...) {...} // catch-all syntax which catches any type of exception Literally 3 dots!

One handler can also deal with part of an error, re-throw the exception to allow someone else to deal with it.

void calculation(DataStructure& ds){
	...
	throw ds_error {...}; // in some if statements
}
 
void DataStructureHandler(DataStructure& ds){
	try{calculation(ds);}
	catch(ds_error e){
		// fix the data structure issue
		throw prompts_input_error{...};
	}
 
}
 
int main(){
	DataStructure ds;
	string s;
	while(cin >> s){
		try{
			DataStructureHandler(ds);
		} catch(prompt_input_error e){
			cout << "Invalid Input";
		}
	}
}

The design of having multiple handlers allows for different parts of the code to handle specific types of exceptions. In this scenario, the DataStructureHandler() function deals with errors related to the DataStructure object, while the main() function handles errors related to invalid user input. By re-throwing exceptions, the program can delegate the responsibility of handling specific exceptions to different parts of the code.

Next: CS247 Lecture 13