GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 5

 

Topic 9: Envelopes and Letters

Introduction

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").


Synopsis


Source Code Examples


The Envelope/Letters concept

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.


Implementing a 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:

The implementation¤ employs one interesting technique: it uses a single 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


Implementing a 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


Implementing letters for the 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:

Note that these are exactly the same semantics as for the 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:

Once again, these are the same semantics as in the original 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:

These semantics are implemented by the 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


Templated letters

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:

Of course, these are not the only (or even the safest) semantics possible in this case. For example, we might choose to be more restrictive and define 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


Specializing templated letters

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


Tying templated letters to the 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


Exercises

There are 11 exercises associated with this topic.

 


This material is part of the GCO4020/CSC428 - Advanced Object Oriented Techniques In C++ course.
Copyright © Damian Conway, 1997. All rights reserved.

Last updated: Fri Feb 18 11:17:41 2000