COMP6771
Advanced C++ Programming
Week 9
Runtime Polymorphism
Key concepts
-
Inheritance: ability to create new classes based on existing ones
- Supported by class derivation
-
Polymorphism: allows objects of a subclass to be used as if they were objects of a base class
- Supported via virtual functions
-
Dynamic binding: run-time resolution of the appropriate function to invoke based on the type of the object
- Closely related to polymorphism
- Supported via virtual functions
Thinking about programming
- Represent concepts with classes
-
Represent relations with inheritance or composition
-
Inheritance: A is also a B, and can do everything B does
- "is a" relationship
- A dog is an animal
-
Composition (data member): A contains a B, but isn't a B itself
- "has a" relationship
- A person has a name
- Choose the right one!
-
Inheritance: A is also a B, and can do everything B does
Protected members
- Protected is a keyword we can use instead of public / private
- Protected members are accessible only to the class, or any subclass of it
Inheritance in C++
- To inherit off classes in C++, we use "class DerivedClass: public BaseClass"
-
Visibility can be one of:
-
public (generally use this unless you have good reason not to)
- If you don't want public, you should (usually) use composition
- protected
- private
-
public (generally use this unless you have good reason not to)
-
Visibility is the maximum visibility allowed
-
If you specify ": private BaseClass", then the maximum visibility is private
- Any BaseClass members that were public or protected are now private
-
If you specify ": private BaseClass", then the maximum visibility is private
Tenets of C++
-
Don't pay for what you don't use
-
C++ Supports OOP
- No runtime performance penalty
-
C++ supports generic programming with the STL and templates
- No runtime performance penalty
-
Polymorphism is extremely powerful, and we need it in C++
-
Do we need polymorphism at all when using inheritance?
- Answer: sometimes
- But how do we do so, considering that we don't want to make anyone who doesn't use it pay a performance penalty
-
Do we need polymorphism at all when using inheritance?
-
C++ Supports OOP
Inheritance and memory layout
This is very important, as it guides the design of everything we discuss this week
class BaseClass {
public:
int get_int_member() { return int_member_; }
std::string get_class_name() {
return "BaseClass"
};
private:
int int_member_;
std::string string_member_;
}
class SubClass: public BaseClass {
public:
std::string get_class_name() {
return "SubClass";
}
private:
std::vector<int> vector_member_;
std::unique_ptr<int> ptr_member_;
}
BaseClass object
int_member_ string_member_ |
SubClass object
int_member_ string_member_ |
vector_member_ ptr_member_ |
BaseClass subobject
SubClass subobject
Inheritance and constructors
- Every subclass constructor must call a base class constructor
- If none is manually called, the default constructor is used
- A subclass cannot initialise fields defined in the base class
- Abstract classes must have constructors
class BaseClass {
public:
BaseClass(int member): int_member_{member} {}
private:
int int_member_;
std::string string_member_;
}
class SubClass: public BaseClass {
public:
SubClass(int member, std::unique_ptr<int>&& ptr): BaseClass(member), ptr_member_(std::move(ptr)) {}
// Won't compile.
SubClass(int member, std::unique_ptr<int>&& ptr): int_member_(member), ptr_member_(std::move(ptr)) {}
private:
std::vector<int> vector_member_;
std::unique_ptr<int> ptr_member_;
}
Polymorphism and values
- How many bytes is a BaseClass instance?
- How many bytes is a SubClass instance?
- One of the guiding principles of C++ is "You don't pay for what you don't use"
- Let's discuss the following code, but pay great consideration to the memory layout
class BaseClass {
public:
int get_member() { return member_; }
std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
std::string get_class_name() {
return "SubClass";
}
private:
int subclass_data_;
}
void print_stuff(BaseClass base) {
std::cout << base.get_class_name()
<< ' ' << base.get_member()
<< '\n';
}
int main() {
BaseClass base_class;
SubClass subclass;
print_class_name(base_class);
print_class_name(subclass);
}
The object slicing problem
- If you declare a BaseClass variable, how big is it?
- How can the compiler allocate space for it on the stack, when it doesn't know how big it could be?
- The solution: since we care about performance, a BaseClass can only store a BaseClass, not a SubClass
- If we try to fill that value with a SubClass, then it just fills it with the BaseClass subobject, and drops the SubClass subobject
class BaseClass {
public:
int get_member() { return member_; }
std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
std::string get_class_name() {
return "SubClass";
}
private:
int subclass_data_;
}
void print_stuff(BaseClass base) {
std::cout << base.get_class_name()
<< ' ' << base.get_member()
<< '\n';
}
int main() {
BaseClass base_class;
SubClass subclass;
print_class_name(base_class);
print_class_name(subclass);
}
Polymorphism and References
- How big is a reference/pointer to a BaseClass
- How big is a reference/pointer to a SubClass
- Object slicing problem solved (but still another problem)
-
One of the guiding principles of C++ is "You don't pay for what you don't use"
- How does the compiler decide which version of GetClassName to call?
- When does the compiler decide this? Compile or runtime?
- How can it ensure that calling GetMember doesn't have similar overhead
- How does the compiler decide which version of GetClassName to call?
class BaseClass {
public:
int get_member() { return member_; }
std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
std::string get_class_name() {
return "SubClass";
}
private:
int subclass_data_;
}
void print_stuff(const BaseClass& base) {
std::cout << base.get_class_name()
<< ' ' << base.get_member()
<< '\n';
}
int main() {
BaseClass base_class;
SubClass subclass;
print_class_name(base_class);
print_class_name(subclass);
}
Virtual functions
- How does the compiler decide which version of GetClassName to call?
- How can it ensure that calling GetMember doesn't have similar overhead
- Explicitly tell the compiler that GetClassName is a function designed to be modified by subclasses
- Use the keyword "virtual" in the base class
- Use the keyword "override" in the subclass
class BaseClass {
public:
int get_member() { return member_; }
virtual std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
std::string GetClassName() override {
return "SubClass";
}
private:
int subclass_data_;
}
void print_stuff(const BaseClass& base) {
std::cout << base.get_class_name()
<< ' ' << base.get_member()
<< '\n';
}
int main() {
BaseClass base_class;
SubClass subclass;
print_class_name(base_class);
print_class_name(subclass);
}
Override
- While override isn't required by the compiler, you should always use it
- Override fails to compile if the function doesn't exist in the base class. This helps with:
- Typos
- Refactoring
- Const / non-const methods
- Slightly different signatures
class BaseClass {
public:
int get_member() { return member_; }
virtual std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
// This compiles. But this is a
// different function to the
// BaseClass get_class_name.
std::string get_class_name() const {
return "SubClass";
}
private:
int subclass_data_;
}
Virtual functions
class BaseClass {
public:
virtual std::string get_class_name() {
return "BaseClass"
};
~BaseClass() {
std::cout << "Destructing base class\n";
}
}
class SubClass: public BaseClass {
public:
std::string get_class_name() override {
return "SubClass";
}
~SubClass() {
std::cout << "Destructing subclass\n";
}
}
void print_stuff(const BaseClass& base_class) {
std::cout << base_class.GetClassName()
<< ' ' << base_class.GetMember()
<< '\n';
}
int main() {
auto subclass = static_cast<std::unique_ptr<BaseClass>>(
std::make_unique<SubClass>());
std::cout << subclass->get_class_name();
}
- What do we expect to see?
- What do we actually see?
VTables
-
Each class has a VTable stored in the data segment
- A vtable is an array of function pointers that says which definition each virtual function points to for that class
- If the VTable for a class is non-empty, then every member of that class has an additional data member that is a pointer to the vtable
-
When a virtual function is called on a reference or pointer type, then the program actually does the following
- Follow the vtable pointer to get to the vtable
- Increment by an offset, which is a constant for each function
- Follow the function pointer at vtable[offset] and call the function
VTable example
Final
- Specifies to the compiler "this is not virtual for any subclasses"
- If the compiler has a variable of type SubClass&, it now no longer needs to look it up in the vtable
- This means static binding if you have a SubClass&, but dynamic binding for BaseClass&
class BaseClass {
public:
int get_member() { return member_; }
virtual std::string get_class_name() {
return "BaseClass"
};
private:
int member_;
}
class SubClass: public BaseClass {
public:
std::string get_class_name() override final {
return "SubClass";
}
private:
int subclass_data_;
}
Abstract Base Classes (ABCs)
-
Might want to deal with a base class, but the base class by itself is nonsense
- What is the default way to draw a shape? How many sides by default?
- A function takes in a "Clickable"
-
Might want some default behaviour and data, but need others
- All files have a name, but are reads done over the network or from a disk
-
If a class has at least one "abstract" (pure virtual in C++) method, the class is abstract and cannot be constructed
- It can, however, have constructors and destructors
- These provide semantics for constructing and destructing the ABC subobject of any derived classes
Pure virtual functions
- Virtual functions are good for when you have a default implementation that subclasses may want to overwrite
- Sometimes there is no default available
- A pure virtual function specifies a function that a class must override in order to not be abstract
class Shape {
// Your derived class "Circle" may forget to write this.
virtual void draw(Canvas&) {}
// Fails at link time because there's no definition.
virtual void draw(Canvas&);
// Pure virtual function.
virtual void draw(Canvas&) = 0;
};
Creating polymorphic objects
- In a language like Java, everything is a pointer
- This allows for code like on the left
- Not possible in C++ due to objects being stored inline
- If you want to store a polymorphic object, use a pointer
// Java-style C++ here
// Don't do this.
auto base = std::vector<BaseClass>();
base.push_back(BaseClass{});
base.push_back(SubClass1{});
base.push_back(SubClass2{});
// Good C++ code
// But there's a potential problem here.
// (*very* hard to spot)
auto base = std::vector<std::unique_ptr<BaseClass>>();
base.push_back(std::make_unique<BaseClass>());
base.push_back(std::make_unique<Subclass1>());
base.push_back(std::make_unique<Subclass2>());
Destructing polymorphic objects
- Which constructor is called?
- Which destructor is called?
- What could the problem be?
- What would the consequences be?
- How might we fix it, using the techniques we've already learnt?
// Simplification of previous slides code.
auto base = std::make_unique<BaseClass>();
auto subclass = std::make_unique<Subclass>();
Destructing polymorphic objects
- Whenever you write a class intended to be inherited from, always make your destructor virtual
- Remember: When you declare a destructor, the move constructor and assignment are not synthesized
class BaseClass {
BaseClass(BaseClass&&) = default;
BaseClass& operator=(BaseClass&&) = default;
virtual ~BaseClass() = default;
}
Forgetting this can be a hard bug to spot
Static and dynamic types
- Static type is the type it is declared as
- Dynamic type is the type of the object itself
- Static means compile-time, and dynamic means runtime
- Due to object slicing, an object that is neither reference or pointer always has the same static and dynamic type
int main() {
auto base_class = BaseClass();
auto subclass = SubClass();
auto sub_copy = subclass;
// The following could all be replaced with pointers
// and have the same effect.
const BaseClass& base_to_base{base_class};
// Another reason to use auto - you can't accidentally do this.
const BaseClass& base_to_sub{subclass};
// Fails to compile
const SubClass& sub_to_base{base_class};
const SubClass& sub_to_sub{subclass};
// Fails to compile (even though it refers to at a sub);
const SubClass& sub_to_base_to_sub{base_to_sub};
}
Quiz - What's the static and dynamic types of each of these?
Static and dynamic binding
- Static binding: Decide which function to call at compile time (based on static type)
- Dynamic binding: Decide which function to call at runtime (based on dynamic type)
- C++
- Statically typed (types are calculated at compile time)
- Static binding for non-virtual functions
- Dynamic binding for virtual functions
- Java
- Statically typed
- Dynamic binding
Up-casting
- Casting from a derived class to a base class is called up-casting
- This cast is always safe
- All dogs are animals
- Because the cast is always safe, C++ allows this as an implicit cast
- One of the reasons to use auto is that it avoids implicit casts
auto dog = Dog();
// Up-cast with references.
Animal& animal = dog;
// Up-cast with pointers.
Animal* animal = &dog;
// What's this (hint: not an up-cast)?
Animal animal{dog};
Down-casting
- Casting from a base class to a derived class is called down-casting
- This cast is not safe
- Not all animals are dogs
auto dog = Dog();
auto cat = Cat();
Animal& animal_dog{dog};
Animal& animal_cat{cat};
// Attempt to down-cast with references.
// Neither of these compile.
// Why not?
Dog& dog_ref{animal_dog};
Dog& dog_ref{animal_cat};
How to down cast
- The compiler doesn't know if an Animal happens to be a Dog
- If you know it is, you can use static_cast
- Otherwise, you can use dynamic_cast
- Returns null pointer for pointer types if it doesn't match
- Throws exceptions for reference types if it doesn't match
auto dog = Dog();
auto cat = Cat();
Animal& animal_dog{dog};
Animal& animal_cat{cat};
// Attempt to down-cast with pointers.
Dog* dog_ref{static_cast<Dog*>(&animal_dog)};
Dog* dog_ref{dynamic_cast<Dog*>(&animal_dog)};
// Undefined behaviour (incorrect static cast).
Dog* dog_ref{static_cast<Dog*>(&animal_cat)};
// returns null pointer
Dog* dog_ref{dynamic_cast<Dog*>(&animal_cat)};
auto dog = Dog();
auto cat = Cat();
Animal& animal_dog{dog};
Animal& animal_cat{cat};
// Attempt to down-cast with references.
Dog& dog_ref{static_cast<Dog&>(animal_dog)};
Dog& dog_ref{dynamic_cast<Dog&>(animal_dog)};
// Undefined behaviour (incorrect static cast).
Dog& dog_ref{static_cast<Dog&>(animal_cat)};
// Throws exception
Dog& dog_ref{dynamic_cast<Dog&>(animal_cat)};
Types of functions
Syntax | Name | Meaning |
---|---|---|
virtual void fn() = 0; | pure virtual | Inherit interface only |
virtual void fn() {} | virtual | Inherit interface with optional implementation |
void fn() {} | nonvirtual | Inherit interface and mandatory implementation |
Note: nonvirtuals can be hidden by writing a function with the same name in a subclass
DO NOT DO THIS
Covariants
- If a function overrides a base, which type can it return?
- If a base specifies that it returns a LandAnimal, a derived also needs to return a LandAnimal
- Every possible return type for the derived must be a valid return type for the base
class Base {
virtual LandAnimal& get_favorite_animal();
};
class Derived: public Base {
// Fails to compile: Not all animals are land animals.
Animal& get_favorite_animal() override;
// Compiles: All land animals are land animals.
LandAnimal& get_favorite_animal() override;
// Compiles: All dogs are land animals.
Dog& get_favorite_animal() override;
};
Contravariants
- If a function overrides a base, which types can it take in?
- If a base specifies that it takes in a LandAnimal, a LandAnimal must always be valid input in the derived
- Every possible parameter to the base must be a possible parameter for the derived
class Base {
virtual void use_animal(LandAnimal&);
};
class Derived: public Base {
// Compiles: All land animals are valid input (animals).
void use_animal(Animal&) override;
// Compiles: All land animals are valid input (land animals).
void use_animal(LandAnimal&) override;
// Fails to compile: Not All land animals are valid input (dogs).
void use_animal(Dog&) override;
};
Default arguments and virtuals
- Default arguments are determined at compile time for efficiency's sake
- Hence, default arguments need to use the static type of the function
- Avoid default arguments when overriding virtual functions
class Base {
virtual void PrintNum(int i = 1) {
std::cout << "Base " << i << '\n';
}
};
class Derived: public Base {
void PrintNum(int i = 2) override {
std::cout << "Derived " << i << '\n';
}
};
int main() {
Derived derived;
Base& base;
derived.PrintNum(); // Prints "Derived 2"
base->PrintNum(); // Prints "Derived 1"
}
Construction of derived classes
- Base classes are always constructed before the derived class is constructed
- The base class ctor never depends on the members of the derived class
- The derived class ctor may be dependent on the members of the base class
class Animal {...}
class LandAnimal: public Animal {...}
class Dog: public LandAnimals {...}
Dog d;
// Dog() calls LandAnimal()
// LandAnimal() calls Animal()
// Animal members constructed using initialiser list
// Animal constructor body runs
// LandAnimal members constructed using initialiser list
// LandAnimal constructor body runs
// Dog members constructed using initialiser list
// Dog constructor body runs
Virtuals in constructors
If a class is not fully constructed, cannot perform dynamic binding
class Animal {...};
class LandAnimal: public Animal {
LandAnimal() {
Run();
}
virtual void Run() {
std::cout << "Land animal running\n";
}
};
class Dog: public LandAnimals {
void Run() override {
std::cout << "Dog running\n";
}
};
// When the LandAnimal constructor is being called,
// the Dog part of the object has not been constructed yet.
// C++ chooses to not allow dynamic binding in constructors
// because Dog::Run() might depend upon Dog's members.
Dog d;
Destruction of derived classes
- Easy to remember order: Always opposite to construction order
class Animal {...}
class LandAnimal: public Animal {...}
class Dog: public LandAnimals {...}
auto d = Dog();
// ~Dog() destructor body runs
// Dog members destructed in reverse order of declaration
// ~LandAnimal() destructor body runs
// LandAnimal members destructed in reverse order of declaration
// ~Animal() destructor body runs
// Animal members destructed in reverse order of declaration.
Virtuals in destructors
- If a class is partially destructed, cannot perform dynamic binding
- Unrelated to the destructor itself being virtual
class Animal {...};
class LandAnimal: public Animal {
virtual ~LandAnimal() {
Run();
}
virtual void Run() {
std::cout << "Land animal running\n";
}
};
class Dog: public LandAnimals {
void Run() override {
std::cout << "Dog running\n";
}
};
// When the LandAnimal constructor is being called,
// the Dog part of the object has already been destroyed.
// C++ chooses to not allow dynamic binding in destructors
// because Dog::Run() might depend upon Dog's members.
auto d = Dog();
COMP6771 19T2 - 9.1 - Runtime polymorphism
By cs6771
COMP6771 19T2 - 9.1 - Runtime polymorphism
- 1,129