Announcements

My notes from week 3 have been updated! Some additional questions about the copy-and-swap idiom have been depicted and are located here.

Today's Agenda


We've got some dense stuff to go through today, but fear not! Today's schedule will be laden with:

  • Class inheritance

  • More on constructors / destructors

  • Polymorphism

  • Virtual / Pure virtual functions

  • [Time Permitting] Templates

Anything that we do *not* cover, you can consider assigned reading! (though it's really just review, right?)



Class Inheritance

Last week was unfortunately... quite dull... can we do something vaguely resembling entertainment?

Yeah I wish so too... unfortunately, all I have is the following:


I'm glad you guys are here because I'm designing an all new battle game that in no way, shape, or form resembles a certain Nintendo franchise...

Today, we're designing ForneyMon -- a game involving various mythical pets of different types locked in psuedo-humane combat for our amusement!


I'm starting off slow and hoping we can develop the concept a bit in this class... here's the gist:

  • I have two types of ForneyMon right now: the BurnyMon, which singes its opponents with the fire of 5 suns, and DampyMon, which annoys its opponents by getting them wet.

  • Both have a certain amount of starting health and a name that its trainer err... owner has given it.

  • Presently, ForneyMon can only interact by dealing damage and taking damage of a certain type (e.g. Burny damage or Dampy damage).

  • DampyMon take bonus damage from BurnyMon, but DampyMon start with more health.

...anyways here's the sketch I have so far, take a look:


  #include <iostream>
  #include <string>
  using namespace std;
    
  class DampyMon;
  
  class BurnyMon {
      private:
          string m_name;
          int m_health;
  
      public:
          BurnyMon (string name);
          int takeDamage (int dam, string type);
          void dealDamage (DampyMon* other, int dam, string type);
  };
  
  class DampyMon {
      private:
          string m_name;
          int m_health;
  
      public:
          DampyMon (string name);
          int takeDamage (int dam, string type);
          void dealDamage (BurnyMon* other, int dam, string type);
  };

Point out some problems with my class definitions (and don't you dare say the whole example).

  • No ability to talk about groups of ForneyMon, regardless of whether they're DampyMon or BurnyMon

  • Code repetition, especially with private members and constructors

  • No scalability: all of my functions that target other ForneyMon are specific to the class of the other ForneyMon!

Let's look at each of these problems separately and see how we might fix them:


Abstracting Common Members


As we noticed, the private members for our two classes are the same!

Remind me, why is code repetition bad?

It violates the One-Change, One-Place principal, wastes file space, and can potentially hinder efficiency.


If we could somehow abstract those elements outside of these two classes into another class that, say, was higher in a hierarchy, that would be great!

Turns out, that's exactly what we'll do...

A base class is a class from which other (derived) classes inherit properties, like data members and member functions.

We'll define a base class from which our two classes (DampyMon and BurnyMon) inherit some common elements. This makes our two classes that inherit from the base class called derived classes.

A derived class is a class that inherits features (like data members and member functions) from a base class.

Inheritance, therefore, is the process of defining a derived class from an existing base class. We say that the derived class inherits certain characteristics of its base.

The syntax for defining inheritance is the following:

  class DerivedClass: public BaseClass {
      // ...
  };

Here's a simple example of inheritance of a Base class to a Derived class:

  #include <string>
  using namespace std;
  
  struct DropTheBase {
      string sharedDM, baseDM;
      DropTheBase() {
          sharedDM = "[sharedDM] Base!";
          baseDM = "[baseDM] Base!";
      }
  };
  
  // These puns are almost cringey... almost
  struct DerivingMeCrazy : public DropTheBase {
      string sharedDM, derivedDM;
      DerivingMeCrazy() {
          sharedDM = "[sharedDM] Derived!";
          derivedDM = "[derivedDM] Derived!";
      }
  };

So what's happening behind the scenes here? Let's look at a few properties of inheritance:

The is-a relationship describes the inheritance flow from a base class to a derived class. We say that a derived class is-a type of the base class.


Well, during lecture, we said that a Dog (derived class) is-a Mammal (base class).

So, above, we say that DerivingMeCrazy is-a DropTheBase... hmm, not one of my finest examples... but worth the joke.

What this means is that the Base class, and all of its members, are now a PART of the derived class!

[Left] The UML standard for representing inheritance is by drawing an arrow from the Derived classes to their Base class(es); [Right] we can think of inheritance as a Base class object being a sort of hidden data member of a Derived class, but with some extra properties on top (to be described)

See how that relationship works out?

Inheritance Prop 1: any (public or equally visible) member of a Base class can be accessed from its Derived classes (BUT, not the other way around).

See how we access the m member from a DerivingMeCrazy class:

  #include <string>
  #include <iostream>
  using namespace std;
  
  struct DropTheBase {
      string sharedDM, baseDM;
      DropTheBase() {
          sharedDM = "[sharedDM] Base!";
          baseDM = "[baseDM] Base!";
      }
  };
  
  struct DerivingMeCrazy : public DropTheBase {
      string sharedDM, derivedDM;
      DerivingMeCrazy() {
          sharedDM = "[sharedDM] Derived!";
          derivedDM = "[derivedDM] Derived!";
      }
  };
  
  int main() {
    DerivingMeCrazy d;
    cout << d.sharedDM << endl;
    cout << d.baseDM << endl;
    cout << d.derivedDM << endl;
  }

Now that's all fine and good, but why did I get the Derived class versions of a and s over the DropTheBase versions?

Inheritance Prop 2: If a data member is defined in both a Derived class and its Base class (i.e., 2 data members have the same name in each), AND:

  • An object is an instance of the Derived class, then we use the Derived class' definitions of those members.

  • An object is an instance of the Base class, then we use the Base class' definitions of those members.


So, if I have a DropTheBase object, I use the Base class' sharedDM.

And, if I have a DerivingMeCrazy object, I use the Derived class' sharedDM

If a member is not defined in the Derived object's class, the compiler will try to find it in the Base class.

What will the following code print out?

  #include <string>
  #include <iostream>
  using namespace std;
  
  struct DropTheBase {
      string sharedDM, baseDM;
      DropTheBase() {
          sharedDM = "[sharedDM] Base!";
          baseDM = "[baseDM] Base!";
      }
  };
  
  struct DerivingMeCrazy : public DropTheBase {
      string sharedDM, derivedDM;
      DerivingMeCrazy() {
          sharedDM = "[sharedDM] Derived!";
          derivedDM = "[derivedDM] Derived!";
      }
  };
  
  int main() {
      DropTheBase b;
      DerivingMeCrazy d;
    
      cout << b.sharedDM << endl;
      cout << b.baseDM << endl;
    
      cout << d.sharedDM << endl;
      cout << d.derivedDM << endl;
  }

Inheritance Prop 3: If A is a base class of B, B can still be a base class of C.

Observe this "inheritance chain" property in the class below, and then draw a UML representation of the inheritance structure.

  struct Basically {
      int a;
      int b;
      Basically () {a = 1; b = 2;}
  };
  struct Best : public Basically {
      int a;
      int c;
      Best () {a = 3; c = 4;}
  };
  struct Example : public Basically {
      int a;
      int d;
      Example () {a = 5; d = 6;}
  };

Using the classes defined above, will any of the following lines of code need to be removed to allow it to compile? If so, which of the numbered lines, and what will it print out once they're removed?

  int main () {
      Basically base;
      Best best;
      Example ex;
      cout << base.a << endl; // 1
      cout << best.a << endl; // 2
      cout << ex.a << endl;   // 3
      cout << best.b << endl; // 4
      cout << ex.c << endl;   // 5
      cout << ex.d << endl;   // 6
      cout << base.d << endl; // 7
  }

Observe this "inheritance chain" property in the class below, and then draw a UML representation of the inheritance structure.

  struct Basically {
      int a;
      int b;
      Basically () {a = 1; b = 2;}
  };
  struct Best : public Basically {
      int a;
      int c;
      Best () {a = 3; c = 4;}
  };
  // [!] Note the inheritance change!
  struct Example : public Best {
      int a;
      int d;
      Example () {a = 5; d = 6;}
  };

Using the classes defined above, will any of the following lines of code need to be removed to allow it to compile? If so, which of the numbered lines, and what will it print out once they're removed?

  int main () {
      Basically base;
      Best best;
      Example ex;
      cout << base.a << endl; // 1
      cout << best.a << endl; // 2
      cout << ex.a << endl;   // 3
      cout << best.b << endl; // 4
      cout << ex.c << endl;   // 5
      cout << ex.d << endl;   // 6
      cout << base.d << endl; // 7
  }

Now, here comes the magic...

I know that because DerivingMeCrazy inherits from DropTheBase, which means that somewhere in DerivingMeCrazy is a DropTheBase object... how do I access its members?

Inheritance Prop 4: If I have a pointer of type Base class, and it points to an object of one that Base class' Derived classes, then my pointer now points to the Base class portion of that Derived class object.

Here's what that looks like:

So knowing that's how pointers of the Base class behave, what will the following print out?

  #include <string>
  #include <iostream>
  using namespace std;
  
  struct DropTheBase {
      string sharedDM, baseDM;
      DropTheBase() {
          sharedDM = "[sharedDM] Base!";
          baseDM = "[baseDM] Base!";
      }
  };
  
  struct DerivingMeCrazy : public DropTheBase {
      string sharedDM, derivedDM;
      DerivingMeCrazy() {
          sharedDM = "[sharedDM] Derived!";
          derivedDM = "[derivedDM] Derived!";
      }
  };
  
  int main() {
    DerivingMeCrazy d;
    DropTheBase* bPtr = &d;
  
    cout << bPtr->sharedDM << endl;
    cout << bPtr->baseDM << endl;
  }

NOTE: I said that a Base class pointer can point to the Base class portion of one of its Derived class' objects... it does NOT work the other way around.

Will the following code compile? If so, what will it print out?

  int main () {
      DropTheBase b;
      DerivingMeCrazy* dPtr = &b;
      
      cout << dPtr->s << endl;
  }

Will the following code compile? If so, what will it print out?

  int main() {
      DerivingMeCrazy* dPtr = new DropTheBase;
    
      cout << dPtr->baseDM << endl;
      cout << dPtr->sharedDM << endl;
  }

Why does this "restriction" on base / derived class pointers make sense?

Consider an Animal class being a base class of Dog. It makes sense for Dogs to be able to do things that Animals can (since Dogs *are* Animals, and so Animal pointers should be able to refer to Dogs) but not that all Animals can do things that Dogs can (since not all Animals are Dogs, and so Dog pointers should not be able to refer to Animals).


Heterogeneous Collections

Returning to our Forneymon example (kinda left that one hanging in the dust), one of the issues we noticed was that I cannot make a collection (let's say, an array) of ForneyMon that consists of both BurnyMon and DampyMon.

It would be nice if I could tell the compiler that I want to make a sort of type hierarchy where I arrange both of these classes under a more general one...

Hey, how bout that! Turns out I can...

So, let's start by defining a new base class called ForneyMon:

Have the BurnyMon and DampyMon classes inherit from a new ForneyMon base class, which you'll define. Abstract the m_name and m_health members into ForneyMon.

  class ForneyMon {
      private:
          // [!] Need to add members
          
      public:
          ForneyMon (string n, int h);
  };
  
  // [!] Need to change something here
  class BurnyMon {
      private:
          // [!] Need to change something here
          string m_name;
          int m_health;
  
      public:
          BurnyMon (string name);
          int takeDamage (int dam, string type);
          // [!] Need to change something here
          void dealDamage (DampyMon* other, int dam, string type);
  };
  
  // [!] Need to change something here
  class DampyMon {
      private:
          // [!] Need to change something here
          string m_name;
          int m_health;
  
      public:
          DampyMon (string name);
          int takeDamage (int dam, string type);
          // [!] Need to change something here
          void dealDamage (BurnyMon* other, int dam, string type);
  };

Now, all that we're missing is a way to construct our objects and we'll be on our way!

Let's look at how to do that next...



Hey Look, More on Construction

Before we can implement our constructors for the ForneyMon, BurnyMon, and DampyMon classes, we'll need to see all of the construction steps:

The three steps of construction for classes using inheritance are:

  1. Construct this class' base class(es)

  2. Declare and / or instantiate this class' data members (by class declaration or member initialization list)

  3. Execute the body of the constructor

Although the base class is not technically a data member of the derived class, we construct it using the member initialization list of the derived class.

Let's look at each step and then integrate them into our ForneyMon implementation, noting that we construct the base class of the object we're constructing before we do anything else.


What will the following code print out?

  // Ugh this joke is bad...
  struct BasicInstinct {
      string stuff;
      BasicInstinct (string s) {
          cout << "[Base] Constructor!" << endl;
          stuff = s;
      }
  };
    
  struct SoDerivative : public BasicInstinct {
      int i;
      SoDerivative (string s, int j) : BasicInstinct(s) {
          cout << "[Derived] Constructor!" << endl;
          i = j;
      }
  };
    
  int main () {
      SoDerivative a(":D", 3);
      cout << a.i << endl;
      cout << a.stuff << endl;
  }

Will the following code compile? If so, what will it print out?

  struct BasicInstinct {
      string stuff;
      BasicInstinct (string s) {
          cout << "[Base] Constructor!" << endl;
          stuff = s;
      }
  };
    
  struct SoDerivative : public BasicInstinct {
      int i;
      
      // [!] Member initialization of base class removed
      SoDerivative (string s, int j) {
          cout << "[Derived] Constructor!" << endl;
          i = j;
      }
  };
    
  int main () {
      SoDerivative a(":D", 3);
      cout << a.i << endl;
      cout << a.stuff << endl;
  }

Now that we know how construction works, let's implement our constructors for ForneyMon!

Here's what I want in each ForneyMon constructor:

  • Each ForneyMon will be given a name

  • Health is decided by the type of ForneyMon; a BurnyMon will start with 10 health, and a DampyMon with 15.

  // [!] ForneyMon Constructor
  ForneyMon::ForneyMon (string n, int h) {
      m_name = n;
      m_health = h;
  }
  
  // [!] Initialization list constructing Base Class
  // components of each BurnyMon and DampyMon
  // (probably shouldn't be hard-coded but meh)
  BurnyMon::BurnyMon (string n) : ForneyMon(n, 10) {}
  DampyMon::DampyMon (string n) : ForneyMon(n, 15) {}

Inheritance Prop 5: a collection that stores Base class pointers can be used to create a heterogeneous collection in which objects of multiple, different, Derived classes can be stored (more on these later).

Returning to our heterogeneous collections example, and assuming we did the above correctly, I can now say things like:

  int main () {
      ForneyMon* menagere[3];
      menagere[0] = new BurnyMon("Emberliz");
      menagere[1] = new DampyMon("Blastoilet");
      // Now with double the copyright infringement!
      menagere[2] = new BurnyMon("Firefox");
  }

See how I was able to store pointers to (the base class components of) both BurneyMon and DampyMon in that array?! Crazy!

But why is this useful if all I can ever access are the common ForneyMon elements of each?

Let's find out!



Polymorphism

Polymorphism is defined as "the provision of a single interface to entities of different types."

Ugh... let's unpack that:

We have two different types: BurnyMon and DampyMon, and each of them can take damage (i.e., they each have a takeDamage function, the single interface), but the way each handles taking damage is slightly different.

We know that pointers of polymorphic Base classes are type-compatible with objects of their Derived classes...

If we have a ForneyMon pointer to a BurnyMon object vs a ForneyMon pointer to a DampyMon object, do we want each ForneyMon pointer's member function calls to have the exact same behavior?

No! We've already said that some operations (e.g., takeDamage) of DampyMon differ from those of BurnyMon.


So, let's see how to distinguish between member functions.

First off, some definitions and rules:

When we have a Base class pointer to a Derived class object, a function binding determines which function implementation in which class to call when that function name is overloaded (e.g., some function f() defined in both the base and derived class).

Non-virtual member functions will bind to the implementation within the type of the object or pointer that called them (aka static-binding, the default).

What will the following code print out?

  // Too good to just have in one example:
  // (read: I'm too lazy to think of another)
  struct BasicInstinct {
      void yell () {
          cout << "[Base] AIEEE!" << endl;
      }
  };
      
  struct SoDerivative : public BasicInstinct {
      void yell () {
          cout << "[Derived] AIEEE!" << endl;
      }
  };
      
  int main () {
      BasicInstinct base;
      SoDerivative derived;
      BasicInstinct* basePtr = &derived;
  
      base.yell();
      derived.yell();
      basePtr->yell();
  }

Why might it be a problem that derived.yell(); and basePtr->yell(); elicited different behaviors?

There would be no point to having base and derived classes if we couldn't have a Base class pointer to a Derived class that elicits the derived class' member function behavior. We'll look at how to solve that problem shortly.


Inheritance Prop 6:If a function is not defined for a Derived class, but it is for the Base class, then we will use the Base class' implementation.

Will the following code compile? If so, what will it print out?

  struct BasicInstinct {
      void yell () {
          cout << "[Base] AIEEE!" << endl;
      }
  };
      
  struct SoDerivative : public BasicInstinct {
      // >_> <_<
  };
      
  int main () {
      BasicInstinct base;
      SoDerivative derived;
  
      base.yell();
      derived.yell();
  }

You can call a base class' functions from inside a derived class by using the scope access (::) operator:

What will the following code print out?

  struct BasicInstinct {
      void yell () {
          cout << "[Base] AIEEE!" << endl;
      }
  };
      
  struct SoDerivative : public BasicInstinct {
      void yell () {
          BasicInstinct::yell();
      }
  };
      
  int main () {
      BasicInstinct base;
      SoDerivative derived;
      BasicInstinct* basePtr = &derived;
  
      base.yell();
      derived.yell();
      basePtr->yell();
  }

Cool right? So knowing these three rules for non-virtual member functions, let's try implementing takeDamage and dealDamage for ForneyMons... ForneyMen? ForneyMon (plural and singular, like "moose").

I want to define a general ForneyMon takeDamage and then specify any different behavior within the derived classes.

Since there's nothing special about how BurnyMon take damage, let's let the Base class handle that.

However, since I want special handling of how DampyMon take damage, we'll override its implementation.

  // ...
  
  class BurnyMon : public ForneyMon {
      public:
          BurnyMon (string name);
          // [!] takeDamage removed from BurnyMon!
          void dealDamage (ForneyMon* other, int dam, string type);
  };
  
  // ...
  
  // (General) Take damage regardless of the type of attack
  int ForneyMon::takeDamage (int dam, string type) {
      // Reduce the current health by dam amount
      m_health -= dam;
      cout << "[" << type << "] Damage: -" << dam << endl;
      return m_health;
  }
  
  // (DampyMon) Take damage equal to dam UNLESS the type
  // of the attack was burny, in which case take 1 extra
  int DampyMon::takeDamage (int dam, string type) {
      if (type == "burny") {
          dam += 1;
      }
      
      // [!] TODO: Note, I do not have access to the Base
      // class' private members in derived class member
      // functions! What to do, what to do?
      return /* TODO ??? */;
  }

If all went according to plan, I should see the following output -2 and -3 damage respectively:

  int main () {
      // Why do all my variables end with y?
      // It's not even cute...
      BurnyMon scorchy("Scorchy");
      DampyMon puddly("Puddly");
      
      // Damage working as intended!
      scorchy.takeDamage(2, "dampy");
      puddly.takeDamage(2, "burny");
  }

But there's one problem left... what happens if I try the following?

  int main () {
      BurnyMon scorchy("Scorchy");
      DampyMon puddly("Puddly");
      ForneyMon* ptr = &puddly;
      
      scorchy.takeDamage(2, "dampy");
      
      // [!] Issue here:
      ptr->takeDamage(2, "burny");
  }

Uh oh, even though I had a ForneyMon pointer to a DampyMon, I still only took 2 damage from a burny attack... let's see how to fix this next!



Virtual Functions

We've already seen the behavior of static-binding such that we call the member function implementation within the class of the object, or pointer to object, that's calling it.

In the case where we want the function implementation of the Derived class that a Base class pointer points to, then we need dynamic-binding.

Dynamic binding is decided at runtime, and executes the Derived class function implementation of any overloaded Base class function tagged with the keyword virtual.

That's a lot of noise, let's look at it in action:

What will the following code print out?

  struct BasicInstinct {
      // [!] Note the virtual keyword
      virtual void yell () {
          cout << "[Base] AIEEE!" << endl;
      }
  };
        
  struct SoDerivative : public BasicInstinct {
      virtual void yell () {
          cout << "[Derived] AIEEE!" << endl;
      }
  };
        
  int main () {
      BasicInstinct base;
      SoDerivative derived;
      BasicInstinct* basePtr = &derived;
      SoDerivative* derPtr = &derived;
      
      base.yell();
      derived.yell();
      basePtr->yell();
      derPtr->yell();
  }

Woah, so basePtr knew to use the Derived class' implementation? Doesn't it just point to the Base object within the Derived one? How'd it know to use it?

Inheritance Prop 7: Whenever we declare at least one member function of a Base class as virtual, it is considered a virtual class. It, and any of its derived classes, will establish virtual tables that are pointers to the correct function implementation to use for each context.

NOTE: I did NOT have to declare the SoDerivative::yell (); as virtual, though it is clean programming to do so in order to alert users that the overloaded function will be dynamically bound.

So, whenever I create an object with a Base class that has *at least one* virtual function, I create hidden pointers within the object stored in its virtual table.

A virtual table is just an array of pointers to virtual functions that point to the correct implementation at runtime.

We won't talk much about virtual tables, but know that they are hidden members that we, as programmers, don't interface with.


So let's return to the problem of our ForneyMon possibly having the wrong implementation of takeDamage called (when a ForneyMon pointer points to a DampyMon object and calls takeDamage).

I need make only one change to my ForneyMon class to elicit the desired takeDamage behavior. What is it?

Simply declare ForneyMon::takeDamage to be virtual!


With this fix made, what does the following code print out?

  int main () {
      BurnyMon scorchy("Scorchy");
      DampyMon puddly("Puddly");
      ForneyMon* ptr = &puddly;
      
      scorchy.takeDamage(2, "dampy");
      
      // [!] Issue *used to be* here:
      ptr->takeDamage(2, "burny");
  }

Sweet! Well that problem's resolved; here's a summary of what objects map to what function calls:


Pointer Type →
Pointing To ↓

Base Class Pointer

Derived Class Pointer

Base Class Object

Base class function

Error: cannot have derived class pointer to base class

Derived Class Object

[If base has virtual] Derived class function
[Else] Base class function

Derived class function


Of course there's one last thing to consider with our ForneyMon...



Pure Virtual Functions

There's one thing left that we'll discuss about ForneyMon... take the following code for example:

  int main () {
      ForneyMon f("Forneytron", 2000);
      f.takeDamage(0, "NOTHING HARMS FORNEYTRON");
  }

Errr... I've created a ForneyMon... ForneyTron... that's not right, I only wanted to allow BurneyMon and DampyMon to be created!

Like we said in class, we should never be able to create a Mammal (base) object so much as a Dog (derived) object, because Mammal is just an archtype classification.

So, I want to find a way to prevent users from creating ForneyMon objects, because this class is meant to be abstract.

An abstract class is one in which at least one function is declared as pure-virtual. You cannot create objects of abstract classes.

Alright, so what's a pure virtual function?

A pure virtual function is denoted by placing "= 0" after the function signature, meaning that the class in which the signature exists is now an abstract class, and that all classes derived from it must either implement said function or become abstract themselves.

A pure virtual function can still be implemented, but need not be so.


Let's see how that looks...

Will the following code compile? If so, what will it output?

  struct Basically {
      int a;
      int b;
      Basically () {a = 1; b = 2;}
      // [!] Pure virtual function
      virtual int compute () = 0;
  };
  struct Best : public Basically {
      int a;
      int c;
      Best () {a = 3; c = 4;}
      virtual int compute () {return a + b;}
  };
  
  int main () {
      Basically base;
      Best best;
      cout << best.compute() << endl;
  }

Will the following code compile? If so, what will it output?

  struct Basically {
      int a;
      int b;
      Basically () {a = 1; b = 2;}
      // [!] Pure virtual function
      virtual int compute () = 0;
  };
  struct Best : public Basically {
      int a;
      int c;
      Best () {a = 3; c = 4;}
      virtual int compute () {return a + b;}
  };
  // [!] Added another class...
  struct Example : public Best {
      int a;
      int d;
      Example () {a = 5; d = 6;}
  };
  
  int main () {
      Best best;
      Example ex;
      cout << ex.compute() << endl;
  }

Very cool... so, let's look again at our ForneyMon classes:

  class ForneyMon {
      private:
          string m_name;
          int m_health;
      public:
          ForneyMon (string n, int h);
          virtual int takeDamage (int dam, string type);
  };
  
  class BurnyMon : public ForneyMon {
      public:
          BurnyMon (string name);
          // [!] Take damage removed from BurnyMon!
          void dealDamage (ForneyMon* other, int dam, string type);
  };
  
  class DampyMon : public ForneyMon {
      public:
          DampyMon (string name);
          int takeDamage (int dam, string type);
          void dealDamage (ForneyMon* other, int dam, string type);
  };

Is there a function I could make pure virtual so that my ForneyMon class becomes abstract?

Yes! The dealDamage function, which has the same signature for each derived class! Alternately, I could declare, and implement, a pure virtual destructor.


We'll need to implement the dealDamage function in our derived classes now that we declared in pure virtual in our base. Do so now!

  // [!] Hmm, these are the same function! I probably should make
  // this a function of ForneyMon instead... left as an exercise ;)
  void BurnyMon::dealDamage (ForneyMon* other, int dam, string type) {
      other->takeDamage(dam, type);
  }
  void DampyMon::dealDamage (ForneyMon* other, int dam, string type) {
      other->takeDamage(dam, type);
  }

As a final note, if I don't have any functions that I want to make pure virtual, but still want an abstract class, then I can define a pure virtual destructor for the base that I implement.

So, let's make that change now and see if our code behaves as intended...

If everything went according to plan, then the following code should *not* compile:

  // [!] Warning: Should NOT compile if everything went well
  int main () {
      ForneyMon f("FORNEYTRON", 2000);
      cout << f.takeDamage(0, "INVINCIBLLLE") << endl;
  }

Alright! My ForneyMon classes are looking pretty good!

Now, let's abandon them forever!



Destructors & Virtual Destructors

There's a final concern with inheritance that we should consider: the clean up!

We know, of course, that this must involve the destructors.

As it turns out, the destruction process for classes using inheritance is simply the reverse of the construction one:

  1. Execute the destructor body.

  2. Destroy any remaining non-dynamic data members (built-in types are ignored)

  3. Destroy the base class component.

So, let's look back at our constructor example and add some dynamically allocated members:

  struct BasicInstinct {
      // [!] New data member is pointer to
      // dynamically allocated string!
      string* stuff;
      BasicInstinct (string s) {
          cout << "[Base] Constructor!" << endl;
          stuff = new string(s);
      }
      ~BasicInstinct () {
          cout << "[Base] Destructor!" << endl;
          delete stuff;
      }
  };
      
  struct SoDerivative : public BasicInstinct {
      // [!] New data member is pointer to
      // dynamically allocated int array!
      int* i;
      SoDerivative (string s, int* j, int size) : BasicInstinct(s) {
          cout << "[Derived] Constructor!" << endl;
          i = new int[size];
          for (int k = 0; k < size; k++) {
              i[k] = j[k];
          }
      }
      ~SoDerivative () {
          cout << "[Derived] Destructor!" << endl;
          delete[] i;
      }
  };

Remembering our steps of construction and destruction, will there be any memory leaks in the following code? What will it print out?

  int main () {
      int j[] = {1, 2, 3};
      SoDerivative a(":D", j, 3);
  }

Remembering our steps of construction and destruction, will there be any memory leaks in the following code? What will it print out?

  int main () {
      BasicInstinct b("AIEE!!!");
  }

Remembering our steps of construction and destruction, will there be any memory leaks in the following code? What will it print out?

  int main () {
      int j[] = {1, 2, 3};
      BasicInstinct* ptr = new SoDerivative(":(", j, 3);
      delete ptr;
  }

Ruh roh... I see I called my derived constructor (which performs dynamic allocation), but never my derived destructor! Memory leak! What happened?

Why didn't the derived destructor get called in the example above?

Because the base destructor is statically bound due to the pointer being of the Base type!


So, again, we rely on the virtual keyword to save the day by using virtual destructors to get dynamic binding.

A virtual destructor ensures that the proper object destructor is called at runtime, regardless of what type of pointer is having delete called on it.

So, to fix our error above, we simply add the virtual keyword to the Base class' destructor!

  // ...
  // [!] Now, destructor is virtual
  virtual ~BasicInstinct () {
      cout << "[Base] Destructor!" << endl;
      delete stuff;
  }
  // ...

So, let's revisit our problematic example and make sure that the dynamic binding is indeed happening:

  int main () {
      int j[] = {1, 2, 3};
      BasicInstinct* ptr = new SoDerivative(":(", j, 3);
      delete ptr;
  }

Whew! All clear!

So, we see that it's very dangerous to leave a base class without a virtual destructor, even if there are no dynamic members to clean up in the base class.

Rule: if a class is going to be a Base class, give it a virtual destructor, which means you must also implement it, even if the body is blank!

Implement a virtual destructor for the ForneyMon class.

That's it for inheritance and polymorphism... on to the fun stuff...



Templates

If you remember from our discussion of stacks and queues, we said that these were two instances of abstract data types.

This meant that, for example, stacks had the FILO behavior, and as long as this behavior is honored, any stack implementation can decide the details for itself.

We noted that one detail that should be in a stack implementation is that it should be type agnostic, i.e., I can have a stack of whatever types I want:

  int main () {
      stack<int> intsOnInts;
      intsOnInts.push(4);
      intsOnInts.push(20);
      intsOnInts.push(-2);
  
      stack<string> weave;
      weave.push("bad");
      weave.push("puns");
      weave.push("return");
  }

Along with this point, we said that, to keep things simple, we wanted to talk about a stack of only one type (for the present). So:

  int main () {
      stack<int> intsOnInts;
      stack<string> weave;
      // weave.push(-2); BAD!
      // intsOnInts.push("bad"); BAD!
  }

But this begs the question: how do I have stacks that can handle ints, strings, or whatever types I want them to handle? Is there a different stack type defined for whatever type I want to stack?

No! We used templates!

Templates are a C++ language feature that allow functions and classes to operate with generic types.

A generic type is a sort of placeholder for a type that will be matched as needed during compilation.


Using templates, we can define functions and classes that work for a variety of different types without having to explicitly create multiple function and class definitions, one for each type.

So what does the syntax for a template look like? Let's start by discussing them for functions:

A function template is defined for some number of generic types with the following syntax:

  template <typename TypeOneName, typename TypeTwoName, ...>
  returnType nameOfFunction (...parameters...) {
      // ... function body ...
  }

Now, say I wanted to have a function that compares two types and determines which one is "greater."

For many types, we have a good understanding of what this means:

  • An int, int1 is greater than int2 if int1 - int2 > 0; i.e., if the quantity of int1 is greater than int2

  • A char operates the same way, except we examine character codes and perform the same comparison.

  • A string s1 is greater than a string s2 by examining each character in the sequence one by one and determining which comes first in the alphabet.

Without templates, we would need to define 3 different functions for the above:

  int maximum(int i1, int i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  char maximum(char c1, char c2) {
      return (c1 > c2) ? c1 : c2;
  }
  
  string maximum(string s1, string s2) {
      return (s1 > s2) ? s1 : s2;
  }
  
  int main () {
      string s1 = "test",
             s2 = "this";
      cout << maximum(s1, s2) << endl;
  
      int i1 = 20,
          i2 = 8;
      cout << maximum(i1, i2) << endl;
  }

Immediately we see that there are syntactic similarities between our three max functions, not to mention the code repetition.

Let's fix it using a template:

  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  int main () {
      string s1 = "test",
             s2 = "this";
      cout << maximum(s1, s2) << endl;
  
      int i1 = 20,
          i2 = 8;
      cout << maximum(i1, i2) << endl;
  }

Well that's handy... so is this template some magical one-stop function that handles all of these different cases?

NO! Templates work like this:

A template function defines a "prototype" function that, during compilation, will be forked into a matching one for the type that wants to use it.

So, when we call maximum with the 2 strings, our compiler sees the matching template expecting 2 string parameters, and then creates its own, copied function implementation where typename T is replaced by string

Similarly, when the compiler sees maximum called with 2 int parameters, it forks yet another copy from the prototype where typename T is replaced by int

The process by which the compiler finds matches between a function call and a template is called argument deduction.

Remember: templates are not functions, they are *patterns* for functions that are then created by the compiler when they see that they are needed.


Sometimes, the compiler will fail at argument deduction:

Will the following code compile? If so, what will it print?

  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  int main () {
      int i = 22;
      double d = 22.2;
      // [!] Will this work?
      cout << maximum(i, d) << endl;
  }

Hmm, so why didn't that work?

When templates define a generic type, e.g. typename T, then parameters defined in terms of that generic type must be EXACT matches.

This means that template <typename T> T maximum(T i1, T i2) says, "You can match me as long as T i1 and T i2 are EXACTLY the same type."

This strict rule is different than what we're used to because ints and doubles can usually be coerced into one another quite easily.

Such is not the case for templates, where an exact match is required.

A template ambiguity is an error when our compiler fails to deduce the correct argument type for a call to a template function.

To solve this, we can give our compiler a hint as to which implementation we want to use; we do this in our function call by saying:

  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  int main () {
      int i = 22;
      double d = 22.2;
      // [!] Notice the hint we gave to our compiler suggesting
      // that the call to maximum should now expect two doubles,
      // which means we'll simply coerce argument i into a double
      cout << maximum<double>(i, d) << endl;
  }

Cool, so that gives us a little more control over our templates arguments, but what about the following:

Examine how maximum is being used below and its intended use with parameters of type ProtoForneyMon. Will the following code compile?

  class ProtoForneyMon {
      private:
          int m_health;
      public:
          ProtoForneyMon (int h) {
              m_health = h;
          }
          int getHealth () {
              return m_health;
          }
  };
  
  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  int main () {
      ProtoForneyMon p1(10), p2(15);
      // [!] I'd like to compare two ProtoForneyMon
      // (the early sketch of the hit-game ForneyMon)
      // and return the health of the one that has more
      cout << maximum(p1, p2) << endl;
  }

Well that's a problem... I want my maximum function to behave in a way that's an exception to the template!

No problem! We'll just create our own definition for how to handle two ProtoForneyMon parameters:

  // ... ProtoForneyMon defined here ...
  
  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  // [!] Overload maximum with an implementation specific
  // to ProtoForneyMon
  int maximum(ProtoForneyMon p1, ProtoForneyMon p2) {
      int h1 = p1.getHealth(),
          h2 = p2.getHealth();
      return (h1 > h2) ? h1 : h2;
  }
  
  int main () {
      ProtoForneyMon p1(10), p2(15);
      cout << maximum(p1, p2) << endl;
  }

Nice, but why didn't my compiler try to perform argument deduction using the template and still blow up?

The compiler will always check for explicit function specializations (no templating) for function call matches before attempting to perform type deduction using a function template.




Partial Templates

So far we've seen templates where all parameter types are generic, but it's also possible to mingle generic types and explicit ones.

Let's use this section to develop the maxInArray function, which takes in an array of some generic type and then returns the maximum element.

  template <typename T>
  T maximum(T i1, T i2) {
      return (i1 > i2) ? i1 : i2;
  }
  
  template <typename T>
  T maxInArray(T* arr, int size) {
      if (size <= 0) {
          return NULL;
      }
  
      // Seed our max with the first
      // element
      T currentMax = arr[0];
      for (int i = 1; i < size; i++) {
          currentMax = maximum(currentMax, arr[i]);
      }
      return currentMax;
  }
  
  int main () {
      int i[] = {4, 5, 2, 3};
      cout << maxInArray(i, 4) << endl;
  
      string s[] = {"max", "me", "now"};
      cout << maxInArray(s, 3) << endl;
  }

I might also want to be able to provide multiple generic types in a function definition, like comparing two arrays for equivalence.

Let's make an arraysEqual function that is designed to compare two arrays of generic types that support the equivalence operation:

  template <typename Type1, typename Type2>
  bool arraysEquivalent (Type1* arr1, Type2* arr2, int size) {
      for (int i = 0; i < size; i++) {
          if (arr1[i] != arr2[i]) {
              return false;
          }
      }
      return true;
  }
  
  int main () {
      int i[] = {48, 49, 50, 51};
      char c[] = {'0', '1', '2', '3'};
      cout << arraysEquivalent(i, c, 4) << endl;
  
      double d[] = {48, 49.5, 50.3, 51.2};
      cout << arraysEquivalent(i, d, 4) << endl;
  }

Remembering that templates are patterns for functions, we see that the compiler deduced Type1 -> int and Type2 -> char in the first call to arraysEquivalent, and then deduced Type1 -> int and Type2 -> double in the second call.


Summary

When designing function templates, we need to double check three key points for our function calls, pretending we're the compiler trying to make sense of the templates:

  • The call must match a template; if it doesn't, we either need to create an explicit specification to be compatible with the argument types, or give our compiler a template hint in the function call

  • Once a match has been made (successful argument deduction), the matching template's code must compile; recall that our ProtoForneyMon matched the maximum template, but the maximum function compared its parameters using the '>' operator, which was not defined for two ProtoForneyMon.

  • The resulting function, once matched and compiled, must perform the intended behavior; the maximum of two pointer addresses may not behave as intended if the pointers are to elements in different arrays (for example)!


Miscellany

A couple last remarks that don't fit well into any other section:

Attempt to make template function parameters passed by constant reference where possible; since the function could possibly work on large, user-defined types as well as built-in types, the cost of passing parameters by value can be significant.

If you need to initialize a generic type to the "0" equivalent defined by the class (e.g. 0 for ints, "" for strings) you can simply declare (for typename T): T();



Class Templates

Just as we have templates for different function parameters, so can we have templates for our own classes!

This means that whenever I create a new object of a particular type, I can define a type to use for the generic type described in the class template.

We've already seen this in action with the Standard Template Library (STL) stacks, like from the start of this lecture:

  int main () {
      stack<int> intsOnInts;
      intsOnInts.push(4);
      intsOnInts.push(20);
      intsOnInts.push(-2);
  
      stack<string> weave;
      weave.push("bad");
      weave.push("puns");
      weave.push("return");
  }

We know, then, that the stack class must have a class template that allows us to use stack in conjunction with whatever types we want (int and string in our example).


Similar to function templates, class templates allow us to define a multitude of classes from a single pattern.

The syntax is the same as function templates, with one exception; let's look at a simple example:

  template <typename Type1, typename Type2>
  class TwoTypes {
      private:
          Type1 m_t1;
          Type2 m_t2;
      public:
          TwoTypes (Type1 t1, Type2 t2) {
              m_t1 = t1;
              m_t2 = t2;
          }
          
          // [!] What is being returned here?
          // Can we predict what a given argument
          // deduction will resolve to?
          Type1 arbitraryFunc () {
              return m_t1 * m_t2;
          }
  };
  
  int main () {
      TwoTypes<int, int> tII(2, 3);
      cout << tII.arbitraryFunc() << endl;
  
      // [!] This line may give you warnings;
      // do you see why?
      TwoTypes<bool, double> tBD(true, 2.3);
      cout << tBD.arbitraryFunc() << endl;
  }

No surprises there, we're used to templating; there's just one issue if we try to define member functions outside of the class definition:

  template <typename Type1, typename Type2>
  class TwoTypes {
      private:
          Type1 m_t1;
          Type2 m_t2;
      public:
          TwoTypes (Type1 t1, Type2 t2);
          Type1 arbitraryFunc ();
  };
  
  // [!] Observe; I need to list the template types above
  // each function definition
  template <typename Type1, typename Type2>
  // [!] Furthermore, I need to define that this is a function
  // on a TwoTypes object with template types <Type1, Type2>
  TwoTypes<Type1, Type2>::TwoTypes (Type1 t1, Type2 t2) {
      m_t1 = t1;
      m_t2 = t2;
  }
  
  template <typename Type1, typename Type2>
  Type1 TwoTypes<Type1, Type2>::arbitraryFunc () {
      return m_t1 * m_t2;
  }
  
  int main () {
      TwoTypes<int, int> tII(2, 3);
      cout << tII.arbitraryFunc() << endl;
  
      TwoTypes<bool, double> tBD(true, 2.3);
      cout << tBD.arbitraryFunc() << endl;
  }

So, just make sure you treat any template member function definitions outside of the class definition as though they were blind to the template types that the class was created with.

That was a nice sentence.


As a final note, you should be aware that you can program the same explicit specification for template member functions that we did for template functions.

For example, if I wanted TwoTypes' arbitraryFunc to handle bools specially, I could write:

  template <typename Type1, typename Type2>
  class TwoTypes {
      private:
          Type1 m_t1;
          Type2 m_t2;
      public:
          TwoTypes (Type1 t1, Type2 t2);
          Type1 arbitraryFunc ();
  };
  
  template <typename Type1, typename Type2>
  TwoTypes<Type1, Type2>::TwoTypes (Type1 t1, Type2 t2) {
      m_t1 = t1;
      m_t2 = t2;
  }
  
  template <typename Type1, typename Type2>
  Type1 TwoTypes<Type1, Type2>::arbitraryFunc () {
      return m_t1 * m_t2;
  }
  
  // [!] 2 Bool specification
  bool TwoTypes<bool, bool>::arbitraryFunc () {
      cout << "[Bool specification]" << endl;
      return m_t1 || m_t2;
  }
  
  int main () {
      TwoTypes<int, int> tII(2, 3);
      cout << tII.arbitraryFunc() << endl;
  
      TwoTypes<bool, bool> tBD(true, false);
      cout << tBD.arbitraryFunc() << endl;
  }

And that's all we have to say about templates for now! Handy eh?

Not a whole lot new under the sun, but you have some new coding tools under your belt to simplify otherwise bulky code.


[!] WARNING: Template class definitions work differently with file organization than with non-templated classes.


If your code is not compiling because you've tried splitting your templated class definition in a header and then the function implementations in a .cpp, you might be having issues.

This C++ quirk is described here, along with the possible resolutions: Stack Overflow Article


  PDF / Print