Abstract Classes

Steven Zeil

Last modified: Aug 9, 2019
Contents:

Abstract: When we inherit from a base class, we inherit public interfaces of attributes and functions, private data and functions, and implementations (function bodies).

We have seen that subclasses can override inherited function bodies. Indeed, the variant behavior pattern relies on this.

Often, though, we might only want to inherit interfaces. We would then expect all implementations to be provided by the subclasses.

This leads us to the idea of “abstract” or “pure virtual” inheritance, which we explore in this module.

1 Introduction

Abstract inheritance takes place when

This is sometimes called pure inheritance or pure virtual inheritance. It has also been known as the “shared protocol” pattern.

In this pattern,

2 Example: A Library of Varying Data Structures

A common requirement in many libraries is to provide different data structures for the same abstraction.


libg++

 

For example, prior to the standardization of C++, the GNU g++ compiler was distributed with a library called libg++.

This library was extraordinary for the variety of implementations it provided for each major ADT. For example, the library declared a Set class that declared operations like adding an element to a set or checking the set to see if some value were a member of that set. However, the Set class itself contained no data structure that could actually hold any data elements.

Instead, a variety of subclasses of Set were provided. Each subclass implemented the required operations for being a Set, but provided its own data structure for storing and searching for the elements.

In addition to these, there were subclasses for storing the elements in expandable arrays (similar to the std::vector), in dynamically expandable hash tables, in skip lists, etc.

The idea was that each of these data structures offered a slightly different trade off of speed versus storage, sometimes depending upon the size of the sets being manipulated.

Set was only one of many ADTs in the library, but every ADT got this same multiple-implementing-data-structure treatment.

2.1 Working with the Set base class

genset.cpp
void generateSet (int numElements, Set& s, int *expected)
{
  int elements[MaxSetElement];
  

  for (int i = 0; i < MaxSetElement; i++)
    {
      elements[i] = i;
      expected[i] = 0;
    }
  

  // Now scramble the ordering of the elements array
  for (i = 0; i < MaxSetElement; i++)
    {
      int j = rand(MaxSetElement);
      int t = elements[i];
      elements[i] = elements[j];
      elements[j] = t;
    }

  // Insert the first numElements values into s
  s.clear();
  for (i = 0; i < numElements; i++)
    {
      s.add(elements[i]);
      expected[elements[i]] = 1;
    }
}

A programmer could write code, like the code shown here, that could work an any set.

It is only in the code that declared a new set variable (before calling generateSet) that an actual choice of data structure would have to be made.

This meant that it was quite easy to write an application using one kind of set, measure the speed and storage performance, and then to change to a different variant of Set that would be a better match for the desired performance of the application.

libg++ provided similar options, not only for sets, but also for bags (sets that permit duplicates, a.k.a. “multi-sets”), maps, and all manner of other container ADTs.

2.2 Implementing the Set base class

If we are working with libg++ This is OK:

void foo (Set& s, int x)
{
   s.add(x);
   cout << x << " has been added." << endl;
}
 
int main ()
{
   BSTSet s;
   foo (s, 23);
    ⋮

Adding to a General Set

But what should happen here?

 void foo (Set& s, int x)
 {
     s.add(x);
     cout << x << " has been added."   << endl;
 }
 
 int main ()
 {
     Set s;
     foo (s, 23);
       ⋮

3 Abstract Base Classes

How do we prevent programmers from mistakenly creating such useless objects?

3.1 Abstract Member Functions

class Set {
      ⋮
    virtual Set& add (int) = 0;
      ⋮
};
   

3.2 Abstract Classes

An abstract class in C++ is any class that

“Abstract classes” are also known as pure virtual classes.

3.2.1 Set as an Abstract Class

Set in libg++ is a good example of a class that should be abstract.

class Set
{
  public:
     ⋮
    virtual void clear() = 0;
    virtual void add (Element e) = 0;
     ⋮
};

3.3 Limitations of Abstract Classes

Abstract classes carry some limitations, designed to make sure we use them in a safe manner.

class Set {
   ⋮
  virtual Set& add (int) = 0;
   ⋮
};
  ⋮
void foo (Set& s, int x) // OK
  ⋮
 
int main () {
   Set s;  // error!
   foo (s, 23);
     ⋮

4 Examples of Abstract Classes

In our Spreadsheet program,

Look at the declaration of Expression and see which functions are marked as abstract. Ask yourself if you could implement any of those for all possible expressions.

For example, can you write a rule to evaluate any Expression?

Click

Can you write a rule to evaluate a PlusNode? Or perhaps a NumericConstant?

Click

5 Abstract Classes in Java

Abstract classes are also common in Java, and they behave just like abstract classes in C++.

The only difference is in the syntax for declaring them as abstract:

  1. Instead of taking on an “= 0” as in C++, in Java we label the function declaration as abstract.

  2. In C++, if a function member is abstract, the whole class is automatically abstract. In Java, we are also required to label the class itself as abstract.

For example, in C++, if we had

class Set
{
  public:
     ⋮
    virtual void clear() = 0;
    virtual void add (Element e) = 0;
     ⋮
};

the Java equivalent would be

public abstract class Set
{
     ⋮
    public abstract void clear();
    public abstract void add (Element e);
     ⋮
}

For a more concrete example, compare the abstract Value class in C++:

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

to its equivalent in Java:

Value.java
package SpreadSheetJ.Model;


//
// 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.
//
public abstract
class Value implements Cloneable
{
    public abstract String valueKind();
    // Indicates what kind of value this is. For any two values, v1 and v2,
    // v1.valueKind() == v2.valueKind() if and only if they are of the
    // same kind (e.g., two numeric values). The actual character string
    // pointed to by valueKind() may be anything, but should be set to
    // something descriptive as an aid in identification and debugging.
    
    
    public abstract String render (int maxWidth);
    // 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.

    public String toString()
    {
	return render(0);
    }
    
    public boolean equals (Object value)
    {
	Value v = (Value)value;
	return (valueKind() == v.valueKind()) && isEqual(v);
    }
	

    abstract boolean isEqual (Value v);
    //pre: valueKind() == v.valueKind()
    //  Returns true iff this value is equal to v, using a comparison
    //  appropriate to the kind of value.

}


But Java also has another mechanism for inheriting “pure” interfaces, which we will look at in the next lecture.