Programming in C++

Introduction to Object Oriented Programming (OOP)

Inheritance and Dynamic Polymorphism

Gerald Senarclens de Grancy

Purpose of Inheritance

In OOP, inheritance is a fundamental concept that allows
a subclass (aka. derived class or child class)
to inherit properties and methods from
a superclass (aka. base class or parent class)

The two major uses for hierarchies are named implementation inheritance and interface inheritance

classDiagram
  Parent <|-- Child
      

Which problems does inheritance solve?

  • Inheritance best models is-a relationships
  • It reduces code duplication as subclasses inherit public attributes and methods of their superclasses
  • Sibling classes can share code that is defined in a common parent class ⇒ implementation inheritance reduces code size
  • Subclasses can add new attributes and methods or override existing ones without having to change the parent class

Which problems does a inheritance cause?

  • Increased complexity - it is too easy to get inheritance wrong
  • Class hierarchies can become difficult to understand
  • Multiple inheritance risks leading to ambiguity and confusion
  • Composition / aggregation is often a more flexible alternative
  • Non-public inheritance destroys the concept of "is-a relationships"

Access Specifiers in C++ Inheritance

Private members of base classes are never directly accessible in subclasses

public
Public inheritance is the default for structs
Public inheritance means that members of the base class are inherited with the same access specifier
private
Private inheritance is the default for classes
All accessible members of the base class become private members in the derived class
Breaks is-a relationship

Introductory Example

#include <iostream>
using std::cout, std::endl;

class Parent {
public:
  void defined_in_parent() { cout << "method defined in parent" << endl; }
};

class Child : public Parent {  // inherit from `Parent`; rec.: always `public`!

};

int main() {
  Child c{};
  c.defined_in_parent();
  return 0;
}

Visualize execution

Related Guidelines

  • Only use public inheritance
  • Data members should be private
  • Limit protected to member functions that might need to be accessed from subclasses

See Google C++ Style Guide - Inheritance

Polymorphism

Definition

The name literally means "many forms"
(from Greek poly- "many" and morph- "form").

Polymorphism allows an entity (eg.: a function, a class, or an operator) to take on multiple forms or have multiple implementations.

Compile-Time Polymorphism (Static Binding)

Resolved by the compiler before the program executes

Function Overloading
Multiple functions with the same name but different parameter lists
Example: int add(int i, int j) and add(double c, double d) are selected at compile time based on the argument type
Operator Overloading, Operator Overloading for User Defined Types
Operators have different meanings based on the types of the operands
Templates (Generics)
Code that works with any data type, with the type being decided when the template is instantiated

Runtime Polymorphism (Dynamic Polymorphism or Dynamic Binding)

True polymorphic behavior requires a single method call to invoke different implementations based on an object's runtime type

Allows base interface to be used for actions defined in derived classes

Key Concept
Base* pointing to a derived object executes code written in derived class
Liskov Substitution Principle (LSP)
Subtypes should be substitutable for base types
Code written for the base class operates correctly on derived objects

Virtual Functions

Primary mechanism for run-time polymorphism in C++

C++ determines which non-virtual function to call at compile time

Compile time decisions are based on the declared type of an object or pointer, not the object's dynamic (actual) type

Overriding Member Functions

Member functions in subclasses override functions of base classes

virtual

virtual means exactly and only "this is a new virtual function"

Virtual functions are required for achieving runtime polymorphism when dealing with base class pointers to derived class objects

override

Use override to indicate "this is a non-final overrider"

final

Use final to indicate "this is a final overrider"

Example: Bank Accounts

A regular bank account and a children's bank account have a lot in common

Account can be overdrawn while ChildAccount cannot be overdrawn

classDiagram
  Account <|-- ChildAccount
ChildAccount inherits from account

Download complete example

Example: Bank Accounts

#ifndef ACCOUNT_HPP
#define ACCOUNT_HPP

#include <cstdint>
#include <iostream>
#include <string>

namespace bank {

class Account {
public:
  explicit Account(std::string owner) : Account(owner, 0l) {}
  explicit Account(std::string owner, uint32_t deposit);
  void deposit(uint32_t amount);
  virtual uint32_t withdraw(uint32_t amount);
  int64_t balance() const { return balance_; }
  unsigned long number() const { return number_; }
  std::string owner() const { return owner_; }
private:
  int64_t balance_ = 0;
  std::string owner_;
  unsigned long number_;
  static unsigned long next_number_;
};

std::ostream& operator<<(std::ostream& out, const Account& a);

class ChildAccount : public Account {
public:
  ChildAccount(std::string owner) : Account(owner) {}
  ChildAccount(std::string owner, uint32_t deposit)
    : Account(owner, deposit) {}
  uint32_t withdraw(uint32_t amount) override;
};

}  // namespace bank

#endif  // ACCOUNT_HPP
Download account.hpp

Example: Bank Accounts

#include "account.hpp"

#include <iostream>
#include <string>

namespace bank {

unsigned long Account::next_number_ { 7483919474 };

Account::Account(std::string owner, unsigned int deposit)
    : balance_{deposit}, owner_{owner}  {
  number_ = next_number_;
  next_number_++;
}

void Account::deposit(unsigned int amount) {
  balance_ += amount;
}

/* return the amount actually withdrawn */
unsigned int Account::withdraw(unsigned int amount) {
  balance_ -= amount;
  return amount;
}

std::ostream& operator<<(std::ostream& out, const Account& a) {
  out << a.owner() << "'s Account(#" << a.number() << ", ";
  out << a.balance() << " Cents)";
  return out;
}

} // namespace bank
Download account.cpp

Example: Bank Accounts

#include "account.hpp"

namespace bank {

unsigned int ChildAccount::withdraw(unsigned int amount) {
  if (amount > balance()) {
    amount = balance();
  }
  return Account::withdraw(amount);
}

}  // namespace bank
Download childaccount.cpp

Example: Bank Accounts

#include <iostream>
#include <vector>

#include "account.hpp"

using bank::Account, bank::ChildAccount;
using std::cout, std::endl, std::vector;

int main() {
  Account a {"Gerald"};
  cout << a << endl;
  a.deposit(2000);
  cout << a << endl;
  cout << "attempting to withdraw 2500 cents\n";
  a.withdraw(2500);
  cout << a << endl;

  ChildAccount ca {"Eric"};
  ca.deposit(5000);
  cout << ca << endl;
  cout << "attempting to withdraw 100000 leads to actually withdrawing ";
  cout << ca.withdraw(100000) << endl;
  cout << ca << endl;

  cout << "\nworking with a `ChildAccount` pointed to by an `Account*`\n";
  ChildAccount ca2 {"Mary"};
  Account* ap = &ca2;
  cout << *ap << endl;
  ap->deposit(2000);
  cout << *ap << endl;
  cout << "attempting to withdraw 2500 leads to actually withdrawing ";
  cout << ap->withdraw(2500) << endl;  // requires `virtual` in base class
  cout << *ap << endl << endl;

  vector<Account*> accounts{&a, &ca, &ca2};  // polymorphism ftw!
  for (auto& account : accounts) {
    cout << "got " << account->withdraw(100) << " from " << *account << endl;
  }
}
Download main.cpp

Questions
and feedback...