GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 5
In "Topic 8: Wrappers" we saw a technique for implementing a class by "re-interfacing" an existing class. This topic looks at another approach to that idea: "Envelope/Letters".
The principal difference between the two approaches is that with wrappers, the implementation¤ is fixed at compile time, whereas with envelope/letters the implementation¤ may be altered at run time (often "on-the-fly").
Value class without envelope/letters
Value envelope
Value envelope.
ValueEnv envelope class
The concept of an envelope/letters class system is very straightforward. The envelope class is like a wrapper, in that it provides the necessary public interface¤. The letter classes are a variety of related implementation¤ classes¤, one instance of which can be put into each instance of an envelope at any given time.
That "at any give time" is the important part, because the power of an envelope/letter system lies in being able to change letters, as the need for implementation¤ features changes.
For example, in some circumstances it might be appropriate if a Stack
could change its implementation¤ according to its pattern of usage.
That is, if the Stack detected a large number of successive calls to
Pop(), it might choose to switch to an array implementation¤ in which
the RemoveElement() member reclaimed the (now-unused) memory (recall
that the implementation¤ of Array presented in "Topic 1: Implementing an array class"
did not do this, on the assumption that Append() calls
were more frequent than calls to RemoveLast()).
A more interesting example of the use of envelope/letters is in designing
a general Value class - that is, a class whose instances can store objects
of various types: integers, floating point numbers, strings, booleans, etc.
In this case, the envelope may need to switch implementations¤ (that is,
letters) every time a new value of a differing type is assigned to it.
We will explore this particular design problem in the following sections.
Value class without envelope/letters
The most obvious implementation¤ of a Value class doesn't use
the envelope/letters technique (see the
next section for that approach). Instead we define the following
class (see source code listing: ValueSimple.C). Note that, in the interest of
brevity, only a representative subset of the full interface¤ that a
Value class would require is shown:
class Value { enum ValueType { vNull, vInt, vDouble, vBool, vString }; enum { vCount = 5 };
union PolyType { int Int; double Double; bool Bool; };
public: Value(void); Value(int i); Value(double d); Value(bool b); Value(string s); Value(const char* s);
Value& operator= (const Value& v);
Value& operator++(void);
operator bool (void);
Value& operator+=(const Value& v);
private: ValueType myType; PolyType myVal; string myVal_String; };
The private data members store an enumerated value (myType)
indicating the current type of element the Value stores, plus the
value itself (either in the union myVal or in the string member
myVal_String).
The constructors simply set up an object with the appropriate type flag as determined by the initializing value:
Value::Value(void) : myType(vNull) {} Value::Value(int i) : myType(vInt) { myVal.Int = i; } Value::Value(double d) : myType(vDouble) { myVal.Double = d; } Value::Value(bool b) : myType(vBool) { myVal.Bool = b; } Value::Value(string s) : myType(vString), myVal_String(s) {} Value::Value(const char* s) : myType(vString), myVal_String(s) {}
The assignment operator is, strictly speaking, not necessary, since
the automatically generated "member-by-member" assignment operator
would suffice in this case. It is shown here, only for comparison
with the later variants of the Value class which will be given below:
Value& Value::operator= (const Value& v) { if (&v != this) { myType = v.myType; myVal = v.myVal; myVal_String = v.myVal_String; } return *this; }
Note in particular that assignment may change the type of value being stored.
The preincrement operator is shown to illustrate a typical unary
operation on a Value. It consists of a simple switch statement
which selects the appropriate semantics for the increment depending on the
type of value currently stored. Note in particular that incrementing a
string is undefined (and hence automatically changes the type of the
stored value to a "null"):
Value& Value::operator++(void) { switch (myType) { case Value::vInt: myVal.Int++; break; case Value::vDouble: myVal.Double += 1; break; case Value::vBool: myVal.Bool = true; break; case Value::vString: myType = vNull; break; default: break; } return *this; }
The conversion-to-bool operator illustrates a typical conversion
operator for the Value class. As with operator++, it switches to
select the appropriate semantics for its current type of value. Note
that conversion of int and double to bool has the usual C++
semantics, whilst a string is considered true if its length is
non-zero, and a "null" Value is always false:
Value::operator bool (void) { switch (myType) { case Value::vNull: return false; case Value::vInt: return myVal.Int != 0; case Value::vDouble: return myVal.Double != 0; case Value::vBool: return myVal.Bool; case Value::vString: return myVal_String.length() > 0; } }
The add-and-assign operator (operator+=) demonstrates a typical binary
operation between Values. The semantics which have been chosen for this
implementation¤ are quite restrictive:
Value is equivalent to a
simple assignment (that is, the value being added
replaces the "null" value),
Value to anything is a null operation
(that is, the original value is unchanged),
int and/or double)
produces a numeric type of the most appropriate
type (int for int+int, double for the other
three combinations),
strings performs concatenation,
switch to select semantics based on a combination of the two
type flags. This is achieved by generating a single integer value
(using the PAIR macro) in which the higher bits encode one type flag
and the lower bits encode the other (the vCount constant used in PAIR
ensures that the two bit fields do not overlap):
#define PAIR(a,b) (a<<vCount | b)
Value& Value::operator+=(const Value& v) { if (myType==vNull) { return operator=(v); } if (v.myType==vNull) { return *this; }
switch (PAIR(myType,v.myType)) { case PAIR(vInt,vInt): myVal.Int += v.myVal.Int; break;
case PAIR(vDouble,vInt): myVal.Double += v.myVal.Int; break;
case PAIR(vInt,vDouble): myType = vDouble; myVal.Double = myVal.Int + v.myVal.Double; break;
case PAIR(vDouble,vDouble): myVal.Double += v.myVal.Double; break;
case PAIR(vString,vString): myVal_String += v.myVal_String; break;
default: myType = vNull; break; } return *this; }
Note that the special cases involving a "null" Value are handled
first. All other cases are then handled in the switch.
The "PAIR(vInt,vDouble)" is the interesting case, as it requires
the Value to change type.
See also: Exercise 1
See also: Exercise 2
Value envelope
In "Topic 5: Hiding Implementation Details" we have already seen how
to create a class whose implementation¤ is separated from its
interface¤. Not surprisingly, exactly the same approach - that is, using a
single pointer member to which all member functions forward requests
- can be used to create an envelope class such as Value.
For example, here is the Value class reimplemented as an envelope
(see source code listing: ValueLE.C). Note that the class has been renamed
ValueEnv to distinguish it from the version above):
class ValueEnv { public: ValueEnv(void); ValueEnv(int i); ValueEnv(double d); ValueEnv(bool b); ValueEnv(string s); ValueEnv(const char* s); ValueEnv(const ValueEnv& v);
~ValueEnv(void);
operator bool (void);
ValueEnv& operator++(void);
ValueEnv& operator+=(const ValueEnv& v);
ValueEnv& operator= (const ValueEnv& v);
private: class ValueLet* myLetter; };
Note that whilst the public interface¤ is functionally identical to that of
the Value class, the implementation¤ is completely different. The
private ValueLet* pointer member (myLetter) is used to point
to a dynamically allocated object of a type derived from class
ValueLet. These various types of objects are the "letters" which
are stored in the ValueEnv "envelope". Note too that, just like
the Mapping class in "Topic 5: Hiding Implementation Details",
class ValueEnv needs a destructor to clean up its letters:
ValueEnv::~ValueEnv(void) { delete myLetter; }
The base class for letters (ValueLet) is declared as follows:
class ValueLet { public: virtual ValueLet* copy(void) const = 0; virtual bool convertToBool(void) const = 0; virtual void preincrement(ValueLet*&) = 0; virtual void addassign(ValueLet*&, const ValueLet*) = 0;
virtual void assign(ValueLet*& letter_ref, const ValueLet* val) { delete letter_ref; letter_ref = val->copy(); } };
Note first that all of the member functions are virtual and all but
one are pure virtual (ValueLet::assign is not pure virtual because
the semantics of assignment - remove the current letter and replace it
with a copy of the letter being assigned - is common to all the letter
classes).
In each derived letter class, the copy() member function will
simply return a dynamically allocated copy
of the ValueLet object on which it is called. It will be redefined in
all the classes derived from ValueLet so that copying objects of
different classes works polymorphically (that is, the type of copy
returned depends on the type of the original, even though copy() is
called through a pointer to the base class ValueLet).
One important use of ValueLet::copy() is in implementing the
ValueEnv copy constructor:
ValueEnv::ValueEnv(const ValueEnv& v) : myLetter (v.myLetter->copy()) {}
The remaining ValueLet member functions provide the implementation¤ for the
public member functions of class ValueEnv:
ValueEnv::operator bool (void) { return myLetter->convertToBool(); }
ValueEnv& ValueEnv::operator++(void) { myLetter->preincrement(myLetter); return *this; }
ValueEnv& ValueEnv::operator+=(const ValueEnv& v) { myLetter->addassign(myLetter,v.myLetter); return *this; }
ValueEnv& ValueEnv::operator= (const ValueEnv& v) { myLetter->assign(myLetter,v.myLetter); return *this; }
Note that the virtual functions preincrement(), addassign()
and assign() all take the pointer to the current letter
(myLetter) as their first parameter. On the surface this would seem
redundant, since the same pointer value is already available within
these functions (through their this pointer).
However, in each case the first parameter is passed as a ValueLet*& - a
reference to a pointer to a ValueLet. In other words,
a reference to the surrounding ValueEnv's myLetter member is passed
to the virtual functions. This is essential, as it allows those
virtual functions to change (where necessary) the letter pointed to by
myLetter. The implementation¤ of ValueLet::assign() illustrates
the use of this facility.
The various constructors of class ValueEnv do nothing but
initialize the myLetter member with dynamically allocated letters of
various classes derived from ValueLet (see below).
See also: Exercise 3
See also: Exercise 4
Value envelope.
The various letter classes derived from ValueLet implement the
semantics for the various types of value which can be stored in a
ValueEnv. The simplest is the ValueNull class, which
implements the semantics for the various cases equivalent to cases
where myType==vNull in the original Value class above:
class ValueNull : public ValueLet { public: virtual ValueLet* copy (void) const { return new ValueNull; }
virtual bool convertToBool (void) const { return false; }
virtual void preincrement (ValueLet*&) {}
virtual void addassign(ValueLet*& letter_ref, const ValueLet* val) { assign(letter_ref,val); } };
The various components of this definition imply that:
copy() on a ValueNull object returns a pointer
to a new ValueNull object (returned as a ValueLet*),
ValueEnv to bool returns false,
ValueEnv has no effect,
ValueEnv is equivalent to
simply assigning that value to the "null" ValueEnv.
Value
class, except that this version groups all the semantics for
operations on "null" ValueEnvs into one class.
To associate this type of letter with the ValueEnv class we simply
define its "null" constructor to allocate and store a ValueNull object
as the envelope object's letter:
ValueEnv::ValueEnv(void) : myLetter(new ValueNull) {}
In the same way, we can group all the operations (and the storage!) for
a bool value into another class derived from ValueLet:
class ValueBool : public ValueLet { public: ValueBool(bool val) : myVal(val) {}
virtual ValueLet* copy(void) const { return new ValueBool(myVal); }
virtual bool convertToBool (void) const { return myVal; }
virtual void preincrement (ValueLet*&) { myVal=true; }
virtual void addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) != typeid(ValueNull) { delete letter_ref; letter_ref = new ValueNull; } }
private: bool myVal; };
ValueEnv::ValueEnv(bool b) : myLetter (new ValueBool(b)) {}
In this case, the semantics are:
ValueEnv to bool returns
the stored bool value (myVal),
ValueEnv yields true,
ValueEnv to a "bool" ValueEnv
makes the original ValueEnv "null" (indicating
an invalid operation), unless the ValueEnv being
added was "null" , in which case the addition is
ignored.
Value
class above, except that everything to do with operations on bool
values is now localized to the ValueBool letter class.
Note that it was necessary to resort to RTTI to determine whether the letter
being added in ValueBool::addassign() was a "null".
The semantics for operations on "string" ValueEnv's are:
ValueEnv to bool returns true if
the string's length is non-zero.
ValueEnv converts it to a "null",
ValueEnv to a "string"
ValueEnv concatenates the two, adding a "null"
ValueEnv does nothing, and adding any other
type is invalid (and indicates this by changing the
original ValueEnv to a "null").
ValueString letter class:
class ValueString : public ValueLet { public: ValueString(string val) : myVal(val) {}
virtual ValueLet* copy (void) const { return new ValueString(myVal); }
virtual bool convertToBool (void) const { return myVal.length()>0; }
virtual void preincrement (ValueLet*& letter_ref) { delete letter_ref; letter_ref = new ValueNull; }
virtual void addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueNull)) { /*DO NOTHING*/ } else if (typeid(*val) == typeid(ValueString)) { myVal += static_cast<const ValueString*>(val)->myVal } else { delete letter_ref; letter_ref = new ValueNull; } }
private: string myVal; };
ValueEnv::ValueEnv(string s) : myLetter(new ValueString(s)) {} ValueEnv::ValueEnv(const char* s) : myLetter(new ValueString(s)) {}
Note that ValueString::addassign() performs two RTTI checks,
looking for special cases to handle.
The declarations of letter classes to store int and double
values follows exactly the same pattern, complicated only by the fact
that the semantics of these two classes requires them to interact
(because ints can be added to doubles and vice versa).
This requires two extra steps: first, the classes must declare
mutual friendship, and second, the definition of at least one of the classes'
addassign() members must come after the declaration of
both classes. As a result of these two requirements, the ValueInt and
ValueDouble letter classes are defined as follows:
class ValueInt : public ValueLet { public: ValueInt(int val) : myVal(val) {}
int Val(void) const { return myVal; }
virtual ValueLet* copy(void) const { return new ValueInt(myVal); }
virtual bool convertToBool (void) const { return myVal!=0; }
virtual void preincrement (ValueLet*&) { myVal++; }
virtual void addassign (ValueLet*& letter_ref, const ValueLet* val); // DEFINED BELOW, SINCE IT REQUIRES ValueDouble
private: int myVal; };
class ValueDouble : public ValueLet { public: ValueDouble(double val) : myVal(val) {}
double Val(void) const { return myVal; }
virtual ValueLet* copy(void) const { return new ValueDouble(myVal); }
virtual bool convertToBool (void) const { return myVal!=0; }
virtual void preincrement (ValueLet*&) { myVal+=1; }
virtual void addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueInt)) { myVal += static_cast<const ValueInt*>(val)->Val(); } else if (typeid(*val) == typeid(ValueDouble)) { myVal += static_cast<const ValueDouble*>(val)->myVal; } else { delete letter_ref; letter_ref = new ValueNull; } }
private: double myVal; };
void ValueInt::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueInt)) { myVal += static_cast<const ValueInt*>(val)->myVal; } else if (typeid(*val) == typeid(ValueDouble)) { double addval = myVal + static_cast<const ValueDouble*>(val)->Val(); delete letter_ref; letter_ref = new ValueDouble(addval); } else { delete letter_ref; letter_ref = new ValueNull; } }
ValueEnv::ValueEnv(int i) : myLetter(new ValueInt(i)) {} ValueEnv::ValueEnv(double d) : myLetter(new ValueDouble(d)) {}
See also: Exercise 5
See also: Exercise 6
Even a superficial exploration of the various letter classes described above leads to the conclusion that they are remarkably similar in structure, far more so than members of an inheritance hierarchy normally are. This structural similiarity, where the major difference between related classes lies in the types of their data members, naturally leads us to consider reimplementing them as a single templated letter class.
Indeed, this is an entirely practical solution (see source code listing: ValueLET.C),
and considerably reduces the coding required if many different letter
types are to be used. The new structure of the ValueLet
hierarchy is much simpler, consisting of a single base class
(ValueLet) and a single derived template class
(ValueType<T>). The type parameter T determines the specific
type that a given TypeValue object will store.
The abstract ValueLet base class is exactly the same as before and
the derived template class resembles the various letter classes (ValueNull,
ValueBool, ValueInt, etc.):
template <class Type> class ValueType : public ValueLet { public: ValueType(const Type& val);
const Type& Val(void) const;
virtual ValueLet* copy(void) const;
virtual bool convertToBool (void) const;
virtual void preincrement (ValueLet*&);
virtual void addassign (ValueLet*& letter_ref, const ValueLet* val);
virtual void assign (ValueLet*& letter_ref, const ValueLet* val);
private: Type myVal; };
Except for the addition of the Val() member, which was only present in
ValueInt and ValueDouble previously, this template is equivalent
to all of the letter classes described above (except ValueNull, but see
below).
The sole ValueType constructor is used to initialize the myVal
data member (note that this implies that Type is a type with a copy
constructor):
template <class Type> ValueType<Type>::ValueType(const Type& val) : myVal(val) {}
As in classes ValueInt and ValueDouble, the ValueType::Val() member
provides read-only access to the value stored in the letter:
template <class Type> const Type& ValueType<Type>::Val(void) { return myVal; }
The copy member is simply a generalization of the copy members seen
previously. It dynamically allocates a new object of the appropriate
type (namely, ValueType<Type>) and initializes it with (a copy of)
the value currently stored:
template <class Type> ValueLet* ValueType<Type>::copy(void) { return new ValueType<Type>(myVal); }
The remaining member functions provide reasonable defaults for the various
operations they implement. Note that these defaults will not always
suffice (particularly for addassign()), but we will provide
subsequent specializations that correct
their behaviour for particular types.
Note also the definition of the "helper" class Null, which exists only
to provide a type on which to specialize letters representing "null"
values (such a specialization¤ is used, for example, in the definition
of addassign()):
class Null {};
template <class Type> bool ValueType<Type>::convertToBool (void) const { return myVal; }
template <class Type> void ValueType<Type>::preincrement (ValueLet*&) { ++myVal; }
template <class Type> void ValueType<Type>::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueType<Null>)) { return; } else if (typeid(*val) == typeid(ValueType<Type>)) { myVal+=static_cast<const ValueType*>(val)->Val(); } else { delete letter_ref; letter_ref = new ValueType<Null>(Null()); } }
template <class Type> void ValueType<Type>::assign (ValueLet*& letter_ref, const ValueLet* val) { ValueLet::assign(letter_ref,val); }
The (re-)definition of assign() is an interesting case. From the
point-of-view of its behaviour, the definition is entirely redundant,
since it merely forwards the request to the base class's version of
the same virtual function, which is exactly what would happen if the
definition did not exist. However, the existence of the definition has
one important consequence: it allows us to specialize
ValueType::assign() for particular types.
For example, suppose we wished to create a letter type to store
istream objects. Moreover, suppose we wished to honour the
"non-assignability" of istreams (note that the default behaviour
inherited from ValueLet::assign() would permit an istream
stored in a ValueEnv to be "assigned"). However, because the assign() member
is redefined in the templated ValueType class, we can
avoid this undesired behaviour by explicitly specializing
ValueType<istream>::assign() (in this case, to ignore assignments to
a ValueEnv storing a ValueType<istream> letter):
template <> ValueType<istream>::assign(ValueLet*&, ValueLet*) {}
If ValueType had simply inherited the ValueLet::assign()
virtual function without redefining it, this specialization¤ would not
be possible (since there would be no actual templated
ValueType<Type>::assign() member function to specialize).
The default behaviour chosen for ValueType::addassign is also interesting:
operator+=
for that type (this implies a compile-time error if
that type does not provide operator+=),
addassign() so that it fails in all non-trivial cases (since it is
a somewhat unusual operation on most non-arithmetic types):
template <class Type> void ValueType<Type>::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueType<Null>)) { return; } else { delete letter_ref; letter_ref = new ValueType<Null>(Null()); } }
See also: Exercise 7
See also: Exercise 8
In order to provide the same semantics as for the non-templated letter classes, it is necessary to specialize some members of the templated version for particular types.
The default conversion-to-bool requires an automatic conversion to bool from
the parameteric type (Type). This works well for
ValueType<bool>, ValueType<int> and ValueType<double>, but
is inappropriate for "nulls" (which were previously defined to be
always false) and strings (where we chose to make all non-empty
strings true). Hence we provide specializations¤ of
ValueType::convertToBool() which reinstate these semantics for the
corresponding types:
template <> bool ValueType<Null>::convertToBool (void) const { return false; }
template <> bool ValueType<string>::convertToBool (void) const { return myVal.length() > 0; }
Likewise, the special semantics of preincrementing "nulls" and
strings must also be re-instated:
template <> void ValueType<Null>::preincrement (ValueLet*&) {}
template <> void ValueType<string>::preincrement (ValueLet*& letter_ref) { delete letter_ref; letter_ref = new ValueType<Null>(Null()); }
You may like to confirm that these specializations¤ produce the same behaviour
as ValueNull::preincrement() and
ValueString::preincrement()
respectively.
The member function addassign() is, as mentioned previously, a
particularly nasty case since there is no appropriate universal
semantics, except for arithmetic types (and even there, the desire for
automatic type promotions sabotages any attempt at a useful default
behaviour). Hence it is necessary to specialize
ValueType::addassign() for all the interesting types (again you
may wish to confirm that these specializations¤ provide the same
semantics as before):
template <> void ValueType<Null>::addassign (ValueLet*& letter_ref, const ValueLet* val) { assign(letter_ref,val); }
template <> void ValueType<bool>::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueType<Null>)) { return; } else { delete letter_ref; letter_ref = new ValueType<Null>(Null()); } }
template <> void ValueType<double>::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueType<Null>)) { return; } else if (typeid(*val) == typeid(ValueType<int>)) { myVal += static_cast<const ValueType<int>*>(val)->Val(); } else if (typeid(*val) == typeid(ValueType<double>)) { myVal+=static_cast<const ValueType<double>*>(val)->Val(); } else { delete letter_ref; letter_ref = new ValueType<Null>(Null()); } }
template <> void ValueType<int>::addassign (ValueLet*& letter_ref, const ValueLet* val) { if (typeid(*val) == typeid(ValueType<Null>)) { return; } else if (typeid(*val) == typeid(ValueType<int>)) { myVal += static_cast<const ValueType<int>*>(val)->Val(); } else if (typeid(*val) == typeid(ValueType<double>)) { double sum = myVal + static_cast<const ValueType<double>*>(val)->Val(); delete letter_ref; letter_ref = new ValueType<double>(sum); } else { delete letter_ref; letter_ref = new ValueType<Null>(Null()); } }
Note that we don't need to specialize ValueType::assign() or
ValueType::copy() for any type since the default behaviours
provided suffice in all cases.
See also: Exercise 9
ValueEnv envelope class
Finally, having reinstated all the required letter semantics through
specialization¤, we can define appropriate versions of the
ValueEnv constructors to put the appropriate letters in the
ValueEnv envelope (note that these definitions would
replace those given previously):
ValueEnv::ValueEnv(void) : myLetter(new ValueType<Null>(Null()) {}
ValueEnv::ValueEnv(bool val) : myLetter(new ValueType<bool>(val) {}
ValueEnv::ValueEnv(int val) : myLetter(new ValueType<int>(val) {}
ValueEnv::ValueEnv(double val) : myLetter(new ValueType<double>(val) {}
ValueEnv::ValueEnv(string val) : myLetter(new ValueType<string>(val) {}
ValueEnv::ValueEnv(const char* val) : myLetter(new ValueType<string>(val) {}
See also: Exercise 10
See also: Exercise 11
Last updated: Fri Feb 18 11:17:41 2000