CS247 Lecture 14

Last Time: Exception safety, RAII, Smart pointers This Time: Shared_ptrs, Decorator Pattern, Observer Pattern

What if we want shared ownership, where same memory is deleted only if all the pointers pointing to it are deleted?

We can achieve this via std::shared_ptr<T>.

shared_ptr maintains a reference count, which keeps track of how many shared pointers point at this memory. When it hits 0, memory is freed.

{
	// replace ... with args to create a C object
	shared_ptr<C> p = make_shared<C>(...); 
	if (...) {
		shared_ptr<C> q = p;
	} // destructor for q runs, reference count is 1
	...
} // dtor for p runs, ref count is 0, C object is deleted

Shared Pointers are not perfect.

There are still problems with shared_ptrs:

  1. Susceptible if you use the constructor
C* cp = new C{...};
shared_ptr<C> p{cp}; // Incorrect
shared_ptr<C> q{cp}; // Double delete
  • q has no knowledge of p’s existence

Danger

The problem here is that you’re using the constructor of std::shared_ptr to initialize both p and q with the same raw pointer cp. This means both p and q think they own the same object. When both p and q go out of scope, they will each try to delete the object, leading to a double delete and undefined behaviour.

To avoid this issue, you should construct std::shared_ptr using std::make_shared or by directly passing the constructor arguments:

shared_ptr<C> p = make_shared<C>(...);
shared_ptr<C> q = p; // No double delete, shared ownership is managed correctly
  1. Cyclical reference issue
    • If we have multiple shared pointers that point at each other, we have the issue that some pointers may never be deleted
class Node {
	shared_ptr<Edge> myEdges;
	shared_ptr<Node> neighbors;
};
  • myEdges: This shared_ptr points to a collection of Edge objects associated with the current Node. These Edge objects might also contain references to other Node objects, creating a graph-like structure.
  • neighbors: This shared_ptr points to other Node objects that are considered neighbors of the current Node. This establishes a connection between different Node instances in the graph.

full example through ChatGPTt:

class Node {
	public:
	    shared_ptr<Node> next;
};
 
int main() {
    shared_ptr<Node> node1 = make_shared<Node>();
    shared_ptr<Node> node2 = make_shared<Node>();
 
    // Create a cyclical reference
    node1->next = node2;
    node2->next = node1;
 
    return 0;
}

In general, shared ownership is somewhat rare. Take care in constructing your programs. Usually, it suffices to use the following:

  • unique_ptr or object field - ownership/Composition
  • raw pointer or reference - Aggregation/has-a
  • shared_ptr - shared ownership

Exercise: Implement shared_ptr CS247 final practice

We’ll come back to exception safety later.


Design Patterns

  • Provide some standard “good” solution to a common problem
  • One common problem: adding / removing behaviour at run-time with a polymorphic class
  • One example: Windowing system - GUI

If we attempt to make a class for each possible feature, for features, there are possible configurations. Combinatorial explosion!

Solution: Decorator Pattern (Linked List of functionalities)

Abstract Component - Gives the interface for our classes Concrete Component - Implements the “basic” version of the interface

Abstract Decorator - Organize decorators and has-a decorator or the concrete component. Concrete Decorators - Implement the interface, call operation() on the next object in this linked list.

Window* w = new ScrollBar{new Tabbing{new BasicWindow{}}};

ScrollBar Tabbing Basic Window

w->render()

Example: Pizza

class Pizza {
	public:
		virtual ~Pizza(){}
		virtual float price() const = 0;
		vritual string desc() const = 0;
 
}
 
class CrustAndSauce: public Pizza {
	public:
		float price() const override {return 5.99;};
		string desc() const override {return "pizza";};
}
 
class Decorator: public Pizza {
	protected:
		Pizza* next;
	public:
		Decorator(Pizza* next): next{next} {}
		~Decorator() {delete next;}
}
 
class Topping: public Decorator {
	string name;
	public:
		Topping(const string& s, Pizza* p): Decorator{p}, name{s} {}
		float price() const override {
			return 0.75 + next->price();
		}
		string desc() const override {
			return next->desc() + " with" + name;
		}
}
 
class StuffedCrust: public Decorator {
	public:
		StuffedCrust(Pizza* p): Decorator{p} {}
		float price() const override {
			return next->price() + 2.50;
		}
		string desc() const override {
			return next->desc() + " with stuffed crust";
		}
}
 
Pizza* p = new CrustAndSauce{};
p = new Topping{"cheese", p};
p = new Topping{"pepperoni", p};
p = new StuffedCrust{p};
cout << p->price() << " " << p->desc() << endl;
delete p;

Next: CS247 Lecture 15