Dynamic Binding: Class-Appropriate behavior

Steven Zeil

Last modified: Jul 21, 2015

Contents:
1. Dynamic Binding
1.1 What is dynamic binding?
1.2 Dynamic Binding in C++
2. An Example of Dynamic Binding
3. Why is Dynamic Binding Important?
3.1 The Key Pattern to All OOP
3.2 Examples of the key pattern
4. Examples
4.1 Example: Spreadsheet – Rendering Values
4.2 Example: Evaluating a Cell

To be considered “object-oriented”, a language must support inheritance, subtyping, and dynamic binding. We have seen the first two. Now it’s time to look at the third.

If you want to claim that a program is written in an object-oriented style, the code must be designed to take advantage of these three features. Otherwise, it’s just a traditionally styled program written in an object-oriented programming language (OOPL).

1. Dynamic Binding

Dynamic binding is a key factor in allowing different classes to respond to the same message with different methods.

1.1 What is dynamic binding?

Binding is a term used in programming languages to denote the association of information with a symbol. It’s a very general term with many applications.


Binding Function Calls to Bodies

In OOP, we are particularly interested in the
binding of a function body (method) to a function call.

Given the following:

  a = foo(b);

When is the decision made as to what code will be executed for this call to foo?


Compile-Time Binding

  a = foo(b);

In traditionally compiled languages (FORTRAN, PASCAL, C, etc), the decision is made at compile-time.


Run-Time Binding

  a = foo(b);

In traditionally interpreted languages (LISP, BASIC, etc), the decision is made at run-time.


Dynamic Binding = the Happy Medium?

OOPLs typically feature dynamic binding, an “intermediate” choice in which


Dynamic Binding and Programming Languages

Actually, both the class designer and the application programmer have a say in this. Dynamic binding happens only if the class designer says it’s OK for a given function, and then only if the application programmer makes calls to that function in a way that permits dynamic binding.

1.2 Dynamic Binding in C++

Virtual functions


Dynamic Binding in C++: virtual functions

2. An Example of Dynamic Binding

An Animal Inheritance Hierarchy

For this example, we will introduce a simple hierarchy.

class Animal {
public:
  virtual String eats() {return "???";}
  String name() {return "Animal";}
};          

We begin with the base class, Animal, which has two functions.


Plant Eaters

Now we introduce a subclass of Animal that overrides both those functions.

class Herbivore: public Animal {
public:
   virtual String eats() {return "plants";}
   String name() {return "Herbivore";}
};

Cud-Chewers

Now we introduce a subclass of that class.

class Ruminants: public Herbivore {
public:
   virtual String eats() {return "grass";}
   String name() {return "Ruminant";}
};          

Meat Eaters

And another subclass of the original base class.

class Carnivore: public Animal {
public:
   virtual String eats() {return "meat";}
   String name() {return "Carnivore";}
};          

Output Function

It will also be useful in this example to have a simple utility function to print a pair of strings.

void show (String s1, String s2) {
          cout << s1 << " " << s2 << endl;
}          

Finally, we introduce some application code that makes calls on the member functions of our inheritance hierarchy.


Let’s Make Some Calls

 Animal a, *paa, *pah, *par;
 Herbivore h, *phh;
 Ruminant r;
 paa = &a; phh = &h; pah = &h; par = &r;
 
 show(a.name(), a.eats());      // AHRC ?pgm
 show(paa->name(), paa->eats()); // AHRC ?pgm
 show(h.name(), h.eats);        // AHRC ?pgm
 show(phh->name(), phh->eats()); // AHRC ?pgm
 show(pah->name(), pah->eats()); // AHRC ?pgm
 show(par->name(), par->eats()); //AHRC ?pgm

Note the variety of variables we are using.


What’s the output?

 Animal a, *paa, *pah, *par;
 Herbivore h, *phh;
 Ruminant r;
 paa = &a; phh = &h; pah = &h; par = &r;
 
 show(a.name(), a.eats());      // AHRC ?pgm
 show(paa->name(), paa->eats()); // AHRC ?pgm
 show(h.name(), h.eats);        // AHRC ?pgm
 show(phh->name(), phh->eats()); // AHRC ?pgm
 show(pah->name(), pah->eats()); // AHRC ?pgm
 show(par->name(), par->eats()); //AHRC ?pgm

Question: What will be the output of the various show calls?

See if you can work this out for yourself before moving on.

Answer:

When does dynamic binding take us to a different function body that would compile-time binding? Whenever a pointer or references points to a value that is of a subtype of the pointer’s declared target type.

3. Why is Dynamic Binding Important?

Dynamic binding lets us write application code for the superclass that can be applied to the subclasses, taking advantage of the subclasses’ different methods.

3.1 The Key Pattern to All OOP

Collections of Pointers/References to a Base Class

Suppose we have an inheritance hierarchy:

and that we have a collection of pointers or references to the BaseClass.


The Key Pattern to All OOP

Then this code:

BaseClass* x;
for (each x in collection) {
   x->virtualFunction(...);
}

uses dynamic binding to apply subclass-appropriate behavior to each element of a collection.

Thanks to subtyping, that pointer could be pointing to something of type BaseClass or to any of its subclasses.

If we have enough subclasses, we could wind up doing a different function body each time around the loop.


Study this pattern. Once you understand this, you have grasped the essence of OOP!

3.2 Examples of the key pattern

There are lots of variations on this pattern. We can use almost any data structure for the collection.


Example: arrays of Animals

 Animal** animals = new Animal*[numberOfAnimals];
    ⋮
 for (int i = 0; i < numberOfAnimals; ++i)
    cout << animals[i]->name() << " "
        << animals[i]->eats() << endl;


Example: Linked Lists of Animals (C++)

 struct ListNode {
    Animal* data;
    ListNode* next;
 };
 ListNode* head; // start of list
    ⋮
 for (ListNode* current = head; current != 0; current = current->next)
    cout << current->data->name() << " "
         << current->data->eats() << endl;


Example: vector of Animals

 vector<Animal*> animals;
    ⋮
 for (int i = 0; i < animals.size(); ++i)
    cout << animals[i]->name() << " "
         << animals[i]->eats() << endl;


Example: Trees of Animals

 struct TreeNode {
    Animal* data;
    TreeNode* leftChild;
    TreeNode* rightChild;
 };
 TreeNode* root;
 
 void printTree (const TreeNode* t)
 {
   if (t != 0) {
     printTree(t->leftChild);
     cout << t->data->name() << " "
          << t->data->eats() << endl;
     printTree(t->rightChild);
   }
 }
    ⋮
 printTree(root);

This example is a touch more subtle. There’s no loop, but the essential idea is the same. We are still iterating over a collection (in this case, using recursive calls), obtaining at each step a pointer that can point to any of several types in an inheritance hierarchy, and using that pointer to invoke a virtual function.

4. Examples

4.1 Example: Spreadsheet – Rendering Values

Continuing our earlier example:

value.h
#ifndef VALUE_H
#define VALUE_H

#include <string>
#include <typeinfo>

//
// Represents a value that might be obtained for some spreadsheet cell
// when its formula was evaluated.
// 
// Values may come in many forms. At the very least, we can expect that
// our spreadsheet will support numeric and string values, and will
// probably need an "error" or "invalid" value type as well. Later we may 
// want to add addiitonal value kinds, such as currency or dates.
//
class Value
{
public:
  virtual ~Value() {}


  virtual std::string render (unsigned maxWidth) const = 0;
  // Produce a string denoting this value such that the
  // string's length() <= maxWidth (assuming maxWidth > 0)
  // If maxWidth==0, then the output string may be arbitrarily long.
  // This function is intended to supply the text for display in the
  // cells of a spreadsheet.


  virtual Value* clone() const = 0;
  // make a copy of this value

protected:
  virtual bool isEqual (const Value& v) const = 0;
  //pre: typeid(*this) == typeid(v)
  //  Returns true iff this value is equal to v, using a comparison
  //  appropriate to the kind of value.

  friend bool operator== (const Value&, const Value&);
};

inline
bool operator== (const Value& left, const Value& right)
{
  return (typeid(left) == typeid(right))
    && left.isEqual(right);
}

#endif


Displaying a Cell

Here is the code to draw a spreadsheet on the screen.

 void NCursesSpreadSheetView::redraw() const
 {
   drawColumnLabels();
   drawRowLabels();
 
   CellRange shown = showing();
   for (CellName cn = shown.first();
        shown.more(cn); cn = shown.next(cn))
     drawCell(cn);
 }

    After drawing the column and row labels, a call is made to

showing(). That function returns a rectangular block of cell names (a CellRange) representing those cells that are currently visible on the screen, taking into account the window size, where we have scrolled to, etc.

We have a loop that goes through the collection of cell names, invoking drawCell on each one.


drawCell

 void NCursesSpreadSheetView::drawCell
     (CellName name) const
 {
   string cellValue;
   Cell* c = sheet.getCell(name);
   const Value* v = c->getValue();
   if (v != 0)
    {
     cellValue = v->render(theColWidth);
    }
   centerStringInWidth (cellValue,
                         theColWidth);
   // . . . show cellValue on screen . . .
 }

Here we can see that, from the spreadsheet, we get the cell with the given name. Then from that cell we get a pointer to a value. From that pointer we call render.


render()

Now render in value.h is virtual, and various bodies implementing it can be found in classes like

numrender.cpp
std::string NumericValue::render (unsigned maxWidth) const
  // Produce a string denoting this value such that the
  // string's length() <= maxWidth (assuming maxWidth > 0)
  // If maxWidth==0, then the output string may be arbitrarily long.
  // This function is intended to supply the text for display in the
  // cells of a spreadsheet.
{
  char buffer[256];
  for (char precision = '6'; precision > '0'; --precision)
    {
      if (maxWidth > 0)
    {
      sprintf (buffer, "%.1u", maxWidth);
    }
      else
    buffer[0] = 0;
      string format = string("%") + buffer + "." + precision + "g";
      int width = sprintf (buffer, format.c_str(), d);
      if (maxWidth == 0 || width <= maxWidth)
    {
      string result = buffer;
      result.erase(0, result.find_first_not_of(" "));
      return result;
    }
    }
  return string(maxWidth, '*');
}

, NumericValue,

strrender.cpp
std::string StringValue::render (unsigned maxWidth) const
  // Produce a string denoting this value such that the
  // string's length() <= maxWidth (assuming maxWidth > 0)
  // If maxWidth==0, then the output string may be arbitrarily long.
  // This function is intended to supply the text for display in the
  // cells of a spreadsheet.
{
  if (maxWidth == 0 || maxWidth > s.length())
    return s;
  else
    return s.substr(0, maxWidth);
}

, StringValue, and

errrender.cpp
std::string ErrorValue::render (unsigned maxWidth) const
  // Produce a string denoting this value such that the
  // string's length() <= maxWidth (assuming maxWidth > 0)
  // If maxWidth==0, then the output string may be arbitrarily long.
  // This function is intended to supply the text for display in the
  // cells of a spreadsheet.
{
  string s = theValueKindName;
  if (maxWidth == 0 || maxWidth > s.length())
    return s;
  else
    return s.substr(0, maxWidth);
}

, ErrorValue, .

4.2 Example: Evaluating a Cell

celleval.cpp
const Value* Cell::evaluateFormula()
{
  Value* newValue = (theFormula == 0)
    ? new StringValue()
    : theFormula->evaluate(theSheet);

  if (theValue != 0 && *newValue == *theValue)
    delete newValue;
  else
    {
      delete theValue;
      theValue = newValue;
      notifyObservers();
    }
  return theValue;
}

(The exact mechanism for how that trigger works will be explored later.)


operator==

Look at the implementation of operator== in value.h.

numeq.cpp
bool NumericValue::isEqual (const Value& v) const
  //pre: valueKind() == v.valueKind()
  //  Returns true iff this value is equal to v, using a comparison
  //  appropriate to the kind of value.
{
  const NumericValue& vv = dynamic_cast<const NumericValue&>(v);
  return d == vv.d;
}

,

streq.cpp
bool StringValue::isEqual (const Value& v) const
  //pre: valueKind() == v.valueKind()
  //  Returns true iff this value is equal to v, using a comparison
  //  appropriate to the kind of value.
{
  const StringValue& vv = dynamic_cast<const StringValue&>(v);
  return s == vv.s;
}

, and

erreq.cpp
bool ErrorValue::isEqual (const Value& v) const
  //pre: valueKind() == v.valueKind()
  //  Returns true iff this value is equal to v, using a comparison
  //  appropriate to the kind of value.
{
  return false;
}

.