Big Five

Polymorphic Big Five

You need to be very careful with Polymorphic Big Five, because there are a few problems that can occur. Learned these in CS247.

Consider the following example:

Text t1{"polymorphism", "Ross", 500, "C++"};
Text t2{"programming for babies", "LaurierProf", 100, "Python"};
Book& br1 = t1;
Book& br2 = t2;
br2 = br1;
cout << t2;
  • Title, author and length are set, but topic remains unchanged.

Problem 1: Partial Assignment (because of non-virtual)

Book::operator= is defined as non-virtual. We’re calling operator= method on a reference. We use the static type, call Book::operator=, even though br1 and br2 are referencing texts.

Some fields are copied, but not all. This is the partial assignment problem.

How to fix this? Declare Book::operator= as virtual!

class Book {
	...
	public:
	...
		virtual Book& operator=(const Book& other) {
			...
		}
};

In Text, we can’t just have the following:

Text& operator=(const Text& other);

Signature must be the following

Text& Text::operator=(const Book& other) {}

However, trying to fix this partial assignment this way introduces 2 new problems:

  1. We can’t access other’s topic, because it’s a Book, and Book don’t have topics, only Texts do.
  2. other is a Book&, so now, we get the mixed assignment problem…

Problem 2: Mixed Assignment Problem (because of virtual)

Now, the following is legal:

Comic c{...};
Text t{...};
t = c;

We can see that we can set a Text to a Comic, as the Comic on right-hand side can be implicitly converted to a const Book&.

Setting operator= to virtual creates the is the mixed assignment problem, as we can now can set subclass siblings to each other.

In summary

  • Non-virtual operator = leads to partial assignment
  • virtual operator = mixed assignment

To fix this, we need to restructure the Book hierarchy:

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

To make this abstract, we need a Pure Virtual Method. If no other methods make sense to be pure virtual, 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 problems?

  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

Only works since AbstractBook is an abstract class

I am still confused on why the teacher says “only works because it’s abstract class”.

I guess only works is maybe the wrong word. Because we are setting AbstractBook::operator= to protected. we are ensuring that we can’t assign books to one another. If Book was a concrete class, this would be kind of bad, because we DO want to be able to assign books to one another.

TODO BRAINF-CK IDK WHAT’S GOING ON?? Okay, I am getting brainf-cked, wha

  • Is it because of how I saw assignment operator doesn’t get inherited?
class AbstractBook {
	string title, author;
	int length;
	protected:
		AbstractBook& operator=(const AbstractBook& other) = default;
	public:
		AbstractBook(...) {}
		virtual ~AbstractBook() {};
};
 
class Text: public AbstractBook {
	string topic;
	public:
		Text() {}
		void test() {
			Text t1{};
			Text t2{};
			AbstractBook& br2 = t2;
			t1 = br2; // does NOT compile here,why?
		}
};
  • From my understanding, Text is calling operator=(const Abstract& other), which it has access to because of inheritance. But even making the function public does not fix the problem. “no operator ”=” matches these operands”

Also see Method Safety Levels, weird stuff going on for what it means to be able to “access” something.

Other small problem: Destructor

Consider Text’s destructor. Implicitly, 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(){}