Final Practice for CS247

Short Answer Questions

  1. Describe the difference between virtual and multiple inheritance Virtual inheritance is a technique we use during multiple inheritance to allow the derived class that inherited to share the base class fields during inheritance. Since, for multiple inheritance, a class can inherit from more than two classes. Virtual inheritance avoids the diamond problem. Avoids ambiguity and duplication.

  2. Describe how the compiler provided big 5 operate when using inheritance Given in Lecture 11. Basically for the copy and move constructor, the base class field in the inherited class are initialized using its copy constructor, and the field proper to the class itself is initialized with its own constructor. Same thing with move constructor, but since we are dealing with rvalues, we invoke std::move(). For copy and move assignment operators, we must call the base class copy / move assignment operator for the base class portion. Then, we do the copy for the class’s proper field. topic = t.topic or topic = std::move(topic). For the destructor, it needs to be declared virtual` in the superclass.

  3. Describe the conditions necessary to overload a method In order to overload a method, it needs to have the same function name, different number of parameters or different type of parameter or different order. The return type does not play a role since we cannot overload based on return type.

  4. Explain some ways of performing error handling apart from using exceptions, and give their drawbacks

    • We can set a convention, let’s say return to handle errors, but it doesn’t allow our program to return those set conventions.
    • Setting global variables is another way. But hard to control who changed it and everyone can modify it.
    • Have a struct, it’s a C way of doing it, better ways in C++, not too good to access, takes more memory space.
  5. Define stack-unwinding. How does it relate to the concept of RAII? Stack unwinding is the process we go through whenever an error is thrown. It will go back into the stack, in reverse order to look for a catch block that matches the error we’ve thrown?? It will free up the memory we’ve allocated on the way.? Idk how it is related to RAII. Helpful during stack unwinding since the RAII principle ensures that the resource (in this case, the dynamically allocated object) is automatically released when the RAII object (the smart pointer) goes out of scope. This approach helps prevent resource leaks and eliminates the need for manual resource management and explicit memory deallocation.

  6. Why do we re-throw exceptions with “throw” rather than “throw e”?

  7. What type of error occurs when copying a unique_ptr? Run-time or compile-time? Why? Compile-time, since we don’t allow unique-ptr to perform copy, by labelling it with = delete, it will have compile-time error.

  8. What is the purpose of employing design patterns? Design patterns allow programmers to solve design problems when implementing. Solutions

  9. Draw the UML for each of the design patterns discussed in the course: observer, decorator, strategy, visitor, adapter, command. I know Decorator pattern, Observer pattern, Template Method pattern,

  10. List the four types of casts in C++, and explain the purpose and limitations of each. Why do we prefer C++ casts over C-style casts?

  11. Explain why dynamic_cast only works for classes with at least one virtual method defined.

  12. Give an example of a program design that is highly cohesive but also highly coupled. Give an example of a program design that is lowly coupled but also lowly cohesive.

  13. List the 5 SOLID design principles. Give a brief description of each principle, as well as the benefits of applying the principle to your code. S: Single Responsibility Problem O: Open / Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle

  14. Give an example of a design that does not follow the Open/Closed principle. How could the design be adjusted to follow the principle?

  15. Explain the purpose of the template method pattern. Give an example of how you could rewrite a program to follow the template method pattern.

  16. Explain how private/protected inheritance work

  17. Assume there is a class A with some fields, and no virtual methods. B inherits from A, with some fields of it’s own. Draw a memory diagram for a B object. How does it change when you introduce a virtual method to A? How does it change when you introduce multiple inheritance? Multiple virtual inheritance?

  18. Give two uses of the visitor pattern. This is used for

  19. Give two uses of the CRTP pattern. What special considerations are needed when using it in a pre-established inheritance hierarchy?

  20. Define the 4 levels of exception safety guarantee No guarantee Basic guarantee Strong guarantee Nothrow guarantee

  21. What is the benefit to labelling methods noexcept? If a method is noexcept, does that mean it necessarily provides the nothrow guarantee?

Coding Questions

  1. Write a full implementation of shared_ptr. It should have a default constructor, a constructor taking in a ptr of type T, and support all big 5 operations, as well as an overloaded dereference operator.
template<typename T> class shared_ptr {
	private:
		T* ptr;
		size_t* count;	
	public:
		// Default ctor
		shared_ptr() : ptr(nullptr), count(nullptr){}
 
		// constructor with a pointer?
		shared_ptr(T* p): ptr(p), count(1){}
 
		// Copy ctor
		shared_ptr(const shared_ptr<T>& other): ptr{other.ptr}, count{other.count}{
			if(count){
				++(*count);
			}
		}
 
		// Copy assignment
		shared_ptr<T>& operator=(const shared_ptr<T>& other){
			if(this == &other){
				return *this;
			}
 
			// somehow release the memory of current one?
			ptr = other.ptr
			count = other.count
			if(count){
				++(*count);
			}
			
			return *this;
		}
 
		// Move constructor
		shared_ptr(shared_ptr<T>&& other) noexcept: ptr{other.ptr}, count{other.count}{
			other.ptr = nullptr;
			other.count = nullptr;
		}
 
		// Move assignment
		shared_ptr<T>& operator=(shared_ptr<T>&& other) noexcept {
			if(this == &other) return *this;
 
			// release?
			ptr = other.ptr;
			count = other.count;
			other.ptr = nullptr;
			other.count = nullptr;
			return *this;
		}
 
		// Destructor
		~shared_ptr() {
			release();
		}
 
		// Dereference operator
		T& shared_ptr*(){
			return *ptr;
		}
 
 
	private:
	    // Helper function to release memory
	    void release() {
	        if (ref_count) {
	            --(*ref_count);
	            if (*ref_count == 0) {
	                delete ptr;
	                delete ref_count;
	            }
	        }
	    }
};
  1. A class that defines operator< can define the other methods in terms of the < operator. For example: (a > b) is equivalent to (b < a). (a == b) is equivalent to !(a < b) && !(b < a). Write a CRTP class Compareable. Compareable should add the various comparison operators to a class that supports operator<. Give an example of a class that implements operator< and show how Compareable can be used with it. Explain why one might prefer this over an abstract class “AbstractCompareable” that defines the various comparison operators in terms of <, and defines operator< as a virtual method to be overridden in the subclass.
template<typename Derived> class Comparable {
	public:
		friend bool operator>(const Derived& lhs, const Derived& rhs){
			return rhs < lhs;
		}
 
		friend bool 
}
  1. Consider making the game of Chess. Mock up an inheritance hierarchy in UML where a computer class uses the strategy pattern to make moves depending on the difficulty level of the AI. Implement this in C++.

  2. Consider a hierarchy representing a file system. We could have a superclass Entry, then subclasses for things like Files, Directories, links, network devices, etc. Write a hierarchy including at least Entry, File, and Directory, with an accept method taking in a visitor. Write visitors to count the number of bytes in a directory/file, and one to print out all the file names in a directory recursively.

  3. Consider performing manipulations to a string via the command pattern. You start with some initial string s, then have different commands such as Slice (which returns a substring of s given some starting and ending index), Insert (which inserts a string t into s at some index), and Repeat (which causes s to repeat n times). Implement these commands via the Command pattern, along with an Invoker and the ability to undo each command. Try implementing it in the simple way (the invoker simply keeps a history of the state of the string in-between each command) and in the memory efficient way (the commands have code and extra fields to undo their action).

#include <iostream>
#include <string>
#include <vector>
 
using namespace std;
 
class Command {
public:
    virtual ~Command() {}
    virtual void execute(string& str) = 0;
    virtual void undo(string& str) = 0;
};
 
class SliceCommand : public Command {
    int start, end;
    string prevStr;
public:
    SliceCommand(int start, int end) : start(start), end(end) {}
    void execute(string& str) override {
        prevStr = str;
        str = str.substr(start, end - start + 1);
    }
    void undo(string& str) override {
        str = prevStr;
    }
};
 
class InsertCommand : public Command {
    int index;
    string insertStr;
    string prevStr;
public:
    InsertCommand(int index, const string& insertStr) : index(index), insertStr(insertStr) {}
    void execute(string& str) override {
        prevStr = str;
        str.insert(index, insertStr);
    }
    void undo(string& str) override {
        str = prevStr;
    }
};
 
class RepeatCommand : public Command {
    int n;
    string prevStr;
public:
    RepeatCommand(int n) : n(n) {}
    void execute(string& str) override {
        prevStr = str;
        string repeatedStr;
        for (int i = 0; i < n; ++i) {
            repeatedStr += str;
        }
        str = repeatedStr;
    }
    void undo(string& str) override {
        str = prevStr;
    }
};
 
class Invoker {
    string str;
    vector<Command*> history;
public:
    void addCommand(Command* cmd) {
        history.push_back(cmd);
    }
 
    void executeCommands() {
        for (Command* cmd : history) {
            cmd->execute(str);
            cout << "Result: " << str << endl;
        }
    }
 
    void undoCommands() {
        for (int i = history.size() - 1; i >= 0; --i) {
            history[i]->undo(str);
            cout << "Undo: " << str << endl;
        }
    }
};
 
int main() {
    string initialStr = "Hello, world!";
    Invoker invoker;
 
    Command* sliceCmd = new SliceCommand(0, 4);
    Command* insertCmd = new InsertCommand(5, " there");
    Command* repeatCmd = new RepeatCommand(2);
 
    invoker.addCommand(sliceCmd);
    invoker.addCommand(insertCmd);
    invoker.addCommand(repeatCmd);
 
    cout << "Original: " << initialStr << endl;
    invoker.executeCommands();
 
    cout << "\nUndoing commands:\n";
    invoker.undoCommands();
 
    delete sliceCmd;
    delete insertCmd;
    delete repeatCmd;
 
    return 0;
}

Come up with questions he is likely to ask: 1.