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 ofVec3* 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.
- 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)
- 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