CS247 Lecture 11

Last Time: Virtual, Override, destructors, vptr, vtables This Time: Pure virtual, polymorphic arrays, Polymorphic Big 5

Very Interesting!!:

class Shape {
	public:
		virtual float getArea() const;
}
 
class Square: public Shape {
	float length;
	public:
		Square(float length): length{length} {}
		float getArea() const override {
			return length * length;
		}
}
 
class Circle: public Shape {
	float radius;
	public:
		Circle(float radius): radius{radius} {}
		float getArea() const override {
			return pi * radius * radius;
		}
}

If we do not provide an implementation for Shape::getArea(), code won’t link “undefined reference to vtable error”.

We could make Shape::getArea() return 0, or -1 to indicate “no area”, but it’s not natural. Really we want to avoid a definition for Shape::getArea() entirely.

Solution: Declare Shape::getArea() as a pure virtual function. A pure virtual function is allowed to not have an implementation. See pure virtual method.

class Shape{
	public:
		virtual float getArea() const = 0;
};

Declares getArea() as pure virtual by adding = 0. Classes that declare a pure virtual function are called Abstract Class. Abstract classes cannot be instantiated as objects.

A class that overrides all of its parents pure virtual functions is called a concrete class. Concrete classes can be instantiated.

The purpose of abstract classes is to provide a framework to organize and define subclasses.

Polymorphic Arrays
class Vec2 {
	int x, y;
	public:
		Vec2(int x, int y): x{x}, y{y}{}
};
 
class Vec3: public Vec2{
	int z;
	public: 
		Vec3(int x, int y, int z): Vec2{x, y}, z{z}{}
};
 
void f(Vec2* a){
	a[0] = Vec2{7,8};
	a[1] = Vec2{9,10};
}
 
Vec3 myArray[2] = {Vec3{1,2,3}, Vec3{4,5,6}};
f(myArray);

What does f expect:

What it actually looks like: All of Vec2{7,8} is written into myArray[0] . Half of Vec2{9,10} is written into myArray[0]. the other half is myArray[1].

Note

Lesson: Be very careful when using arrays of objects polymorphically. Use an array of pointers: Vec3* myArray[2]; Other solution: vector of Vec3* s

Polymorphic Big 5

Let’s consider Book hierarchy again.

Text t{"polymorphism", "Ross", 500, "C++"};
Text t2{t};

Compiler still provides us a copy constructor, that works as expected.

Let’s look at copy/move constructor and assignment operator to see their definition.

Copy Constructor:

Text:: Text(const Text& t): Book{t}, topic{t.topic}{}

Calls the copy constructor for the Book portion of Text. t is a const Text&, this is implicitly converted to a const Book&. Book does not have a default copy constructor. So we need it in the MIL.

Move Constructor:

Text::Text(Text&& t): Book{std::move(t)}, topic{std::move(t.topic)}

t and t.topic are lvalues, so we’d invoke the copy constructor if we didn’t use std::move. So we use move!

t is an rvalue reference so we know it is safe to steal title, author, length and topic via using std::move to invoke move constructors. (Since rvalue will be gone, ok to modify them and return a random thing.)

Copy Assignment:

Text& Text::operator=(const Text& t){
	Book::operator=(t); // calls the Book assignment operator for book portion
	topic = t.topic;
	return *this;
}
  • Book::operator=(t); calls the Book assignment operator for the Book portion of this

Move Assignment:

Text& Text::operator=(Text&& t){
	Book::operator=(std::move(t));
	topic = std::move(topic);
	return *this;
}

All of these implementations are what the compiler gives by default.

Customize as necessary - for example, if doing manual memory management, you will need to write your own versions of these.

BUT: Are the compiler provided definitions actually that good?

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

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.

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

Our usual signature for Text is the following:

virtual Text& operator=(const Text& other);

Can we just slap an override on the end of this? NO: signature don’t match in 2 places: return type, parameter type.

  1. Return type: this is actually okay: A subclass’s override method can return a subclass of the virtual function’s return type (if it’s a pointer or reference)
  2. Parameter type for overridden functions must match exactly

Signature must be the following:

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

Problem 1) can’t access other’s topic, because it’s a Book, and Book don’t have topics, only Texts do. Problem 2) other is a Book&, so now this is legal:

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

We can set a Text to a Comic on RHS can be implicitly converted to a const Book&.

This is the mixed assignment problem, which is where you can set subclass siblings to each other.

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

To fix this, restructure the book hierarchy. Arrow to AbstractBook UML: Abstract classes and virtual methods are italicized (with stars).


Post Midterm!!

Next: CS247 Lecture 12