COMP6771 Week 9.1

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!

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
  • 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

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

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
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
    1. Follow the vtable pointer to get to the vtable
    2. Increment by an offset, which is a constant for each function
    3. 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;
  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 20T2 - 9.1 - Runtime polymorphism

By cs6771

COMP6771 20T2 - 9.1 - Runtime polymorphism

  • 103
Loading comments...

More from cs6771