C++ : Abstraction


Q74: What's the big deal of separating interface from implementation?
A: Separating interface from implementation is a key to reusable software.
Interfaces are a company's most valuable resources.  Designing an interface
takes longer than whipping together a concrete class which fulfills that
interface.  Furthermore interfaces require the resources of more expensive
people (for better and worse, most companies separate `designers' from
`coders').  Since they're so valuable, they should be protected from being
tarnished by data structures and other artifacts of the implementation (any
data structures you put in a class can never be `revoked' by a derived class,
which is why you want to `separate' the interface from the implementation).

Q75: How do I separate interface from implementation in C++ (like Modula-2)?
A: Short answer: use an ABC (see next question for what an ABC is).

Q76: What is an ABC (`abstract base class')?
A: An ABC corresponds to an abstract concept.  If you asked a Mechanic if he
repaired Vehicles, he'd probably wonder what *kind* of Vehicle you had in mind.
Chances are he doesn't repair space shuttles, ocean liners, bicycles, and
volkswaggon beetles too.  The problem is that the term `Vehicle' is an abstract
concept; you can't build one until you know what kind of vehicle to build.  In
C++, you'd make Vehicle be an ABC, with Bicycle, SpaceShuttle, etc, being
subclasses (an OceanLiner is-a-kind-of-a Vehicle).

In real-world OOP, ABCs show up all over the place.  Technically, an ABC is a
class that has one or more pure virtual member functions (see next question).
You cannot make an object (instance) of an ABC.

Q77: What is a `pure virtual' member function?
A: Some member functions exist in concept, but can't have any actual defn.  Ex:
Suppose I asked you to draw a Shape at location (x,y) that has size 7.2.  You'd
ask me `what kind of shape should I draw', since circles, squares, hexagons,
etc, are drawn differently.  In C++, we indicate the existence of the `draw()'
method, but we recognize it can only be defined in subclasses:
	class Shape {
	  virtual void draw() const = 0;
	  //...                     ^^^--- `=0' means it is `pure virtual'

This pure virtual makes `Shape' an ABC.  The `const' says that invoking the
`draw()' method won't change the Shape object (ie: it won't move around on the
screen, change sizes, etc).  If you want, you can think of it as if the code
were at the nil pointer.

Pure virtuals allow you to express the idea that any actual object created from
a [concrete] class derived from the ABC *will* have the indicated member fn,
but we simply don't have enough information to actually *define* it yet.  They
allow separation of interface from implementation, which ultimately allows
functionally equivalent subclasses to be produced that can `compete' in a free
market sense (a technical version of `market driven economics').

Q78: How can I provide printing for an entire hierarchy rooted at `class X'?
A: Provide a friend operator<< that calls a protected virtual function:
    class X {
      virtual void print(ostream& o) const;  //or `=0;' if `X' is abstract
      friend ostream& operator<<(ostream& o,const X& x) {x.print(o); return o;}

Now all subclasses of X merely provide their own `print(ostream&)const' member
function, and they all share the common `<<' operator.  Friends don't bind
dynamically, but this technique makes them *act* as if they were.

Q79: What is a `virtual destructor'?
A: In general, a virtual fn means to start at the class of the object itself,
not the type of the pointer/ref (`do the right thing based on the actual class
of' is a good way to remember it).  Virtual destructors (dtors) are no
different: start the destruction process `down' at the object's actual class,
rather than `up' at the ptr's class (ie: `destroy yourself using the *correct*
destruction routine').

Virtual destructors are so valuable that some people want compilers to holler
at you if you forget them.  In general there's only one reason *not* to make a
class' dtor virtual: if that class has no virtual fns, the introduction of the
first virtual fn imposes typically 4 bytes overhead in the size of each object
(there's a bit of magic for how C++ `does the right thing', and it boils down
to an extra ptr per object called the `virtual table pointer' or `vptr').

Q80: What is a `virtual constructor'?
A: Technically speaking, there is no such thing.  You can get the effect you
desire by a virtual `clone()' member fn (for copy constructing), or a `fresh()'
member fn (also virtual) which constructs/creates a new object of the same
class but is `fresh' (like the `default' [zero parameter] ctor would do).

The reason ctors can't be virtual is simple: a ctor turns raw bits into a
living object.  Until there's a living respondent to a message, you can't
expect a message to be handled `the right way'.  You can think of ctors as
`class' [static] functions, or as `factories' which churn out objects.
Thinking of ctors as `methods' attached to an object is misleading.

Here is an example of how you could use `clone()' and `fresh()' methods:
	class Set {  //normally this would be a template
	  virtual void insert(int);	//Set of `int'
	  virtual int  remove();
	  virtual Set& clone() const = 0;	//pure virtual; Set is an ABC
	  virtual Set& fresh() const = 0;
	  virtual ~Set() { }	//see on `virtual destructors' for more

	class SetHT : public Set {
	  //a hash table in here
	  Set& clone() const { return *new SetHT(*this); }
	  Set& fresh() const { return *new SetHT(); }

`new SetHT(...)' returns a `SetHT*', so `*new' returns a SetHT&.  A SetHT is-a
Set, so the return value is correct.  The invocation of `SetHT(*this)' is that
of copy construction (`*this' has type `SetHT&').  Although `clone()' returns a
new SetHT, the caller of clone() merely knows he has a Set, not a SetHT (which
is desirable in the case of wanting a `virtual ctor').  `fresh()' is similar,
but it constructs an `empty' SetHT.

Clients can use this as if they were `virtual constructors':
	void client_code(Set& s)
	  Set& s2 = s.clone();
	  Set& s3 = s.fresh();
	  delete &s2;	//relies on destructor being virtual!!
	  delete &s3;	// ditto

This fn will work correctly regardless of how the Set is implemented (hash
table based, AVL tree based, etc).

See above on `separation of interface from implementation' for more.