Abstract Classes
Steven Zeil
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
-
a parent class specifies a common interface for all children
-
but does not itself implement the behavior
This is sometimes called pure inheritance or pure virtual inheritance. It has also been known as the “shared protocol” pattern.
In this pattern,
-
The base class typically has no instances (objects).
-
All of the objects are actually instances of subclasses.a
-
Some operations may not even be implementable in the general base class.
2 Example: A Library of Varying Data Structures
A common requirement in many libraries is to provide different data structures for the same abstraction.
-
Allows application code writers to balance speed and storage requirements against expected size and usage patterns.
-
Allows code in which the only mention of which data structure is being used comes when the actual objects are constructed
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.
-
AVLSet
stored the data in AVL trees (a balanced binary tree) -
BSTSet
stored the data in ordinary binary search trees -
CHSet
stored the data in a conventional hash table -
SLSet
stored the data in a singly-linked list
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
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.
-
By subtyping, whatever function called
generateSet
could pass as a parameter a reference to anAVLSet
,BSTSet
, or any other subclass ofSet
. -
And all of those subclasses would support the clear and add functions that are actually called by
generateSet
.-
By dynamic binding, those calls would be dispatched to the function body appropriate to the actual kind of
Set
that was being worked with.
-
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);
⋮
-
add()
makes no sense ifSet
doesn’t have a data structure that can actually store elements.-
There’s nothing we can actually supply for the body of
Set::add
that is a sensible implementation of that function.(At best, we could have a function body that simply throws a run-time error.)
-
-
In fact,
Set s;
makes no sense.A “raw”
Set
is a useless item. We can’t expect to do anything useful with that variable.- By contrast a reference to a set
Set&
or a pointer to a setSet*
is just fine, because subtyping allows them to hold an address to something that is actually anAVLSet
, or aBSTSet
, or a …
- By contrast a reference to a set
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;
⋮
};
-
The
= 0
indicates (in C++) that no method exists in this class for implementing this message. -
add is called an abstract member function or a pure virtual function.
-
Subclasses must provide the actual methods (bodies) for these functions.
-
3.2 Abstract Classes
An abstract class in C++ is any class that
-
contains an
= 0
annotation on a member function, or -
inherits such a function and does not provide a method for it.
“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;
⋮
};
- We can’t possibly implement
Set::add
orSet::clear
.- we need to do that in its subclasses.
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);
⋮
-
You cannot construct an object whose type is an abstract class.
-
You cannot declare function parameters of an abstract class type when passing parameters “by copy”.
-
but you can pass pointers/references to the abstract class type.
-
4 Examples of Abstract Classes
In our Spreadsheet program,
-
Our spreadsheet Value class is abstract.
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
We cannot expect, for example, to write a
render
function that would work for all values. We have to rely on the subclasses to provide the code for rendering themselves. -
So is our spreadsheet Expression class.
expression.h#ifndef EXPRESSION_H #define EXPRESSION_H #include <string> #include <iostream> #include "cellnameseq.h" class SpreadSheet; class Value; // Expressions can be thought of as trees. Each non-leaf node of the tree // contains an operator, and the children of that node are the subexpressions // (operands) that the operator operates upon. Constants, cell references, // and the like form the leaves of the tree. // // For example, the expression (a2 + 2) * c26 is equivalent to the tree: // // * // / \ // + c26 // / \ // a2 2 class Expression { public: virtual ~Expression() {} // How many operands does this expression node have? virtual unsigned arity() const = 0; // Get the k_th operand virtual const Expression* operand(unsigned k) const = 0; //pre: k < arity() // Evaluate this expression virtual Value* evaluate(const SpreadSheet&) const = 0; // Copy this expression (deep copy), altering any cell references // by the indicated offsets except where the row or column is "fixed" // by a preceding $. E.g., if e is 2*D4+C$2/$A$1, then // e.copy(1,2) is 2*E6+D$2/$A$1, e.copy(-1,4) is 2*C8+B$2/$A$1 virtual Expression* clone (int colOffset, int rowOffset) const = 0; virtual CellNameSequence collectReferences() const; static Expression* get (std::istream& in, char terminator); static Expression* get (const std::string& in); virtual void put (std::ostream& out) const; // The following control how the expression gets printed by // the default implementation of put(ostream&) virtual bool isInline() const = 0; // if false, print as functionName(comma-separated-list) // if true, print in inline form virtual int precedence() const = 0; // Parentheses are placed around an expression whenever its precedence // is lower than the precedence of an operator (expression) applied to it. // E.g., * has higher precedence than +, so we print 3*(a1+1) but not // (3*a1)+1 virtual string getOperator() const = 0; // Returns the name of the operator for printing purposes. // For constants, this is the string version of the constant value. }; inline std::istream& operator>> (std::istream& in, Expression*& e) { string line; getline(in, line); e = Expression::get (line); return in; } inline std::ostream& operator<< (std::ostream& out, const Expression& e) { e.put (out); return out; } inline std::ostream& operator<< (std::ostream& out, const Expression* e) { e->put (out); return out; } #endif
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
?
Can you write a rule to evaluate a PlusNode
? Or perhaps a NumericConstant
?
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:
-
Instead of taking on an “
= 0
” as in C++, in Java we label the function declaration asabstract
. -
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++:
#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:
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.