GCO4020/CSC428 - Advanced Object Oriented Techniques In C++
Week 4
The "Wrapper" technique provides a simple means of implementing classes in terms of existing classes. Wrappers are distinctive in that they (usually) have no private data members of their own, but rely entirely on those inherited from their implementation¤ class¤.
This topic illustrates a straightforward wrapper class which implements a simple stack over an existing array class, and then discusses the pro's and con's of using wrappers.
For an extended discussion on wrappers and related idioms see (Gamma et al. 1995, Chapter 4: "Adapters").
A "wrapper" class is a class which changes the interface¤ of an
existing class, without altering or extending the implementation¤ in any
way. For example, a Stack class may be implemented simply by changing
the interface¤ of an existing Array class (see "Topic 1: Implementing an array class"), by replacing "indexed access" through Array::operator[]
with "end only" access via Stack::Push() and Stack::Pop().
Such a wrapped Stack class could be defined as follows
(see source code listing: Stack.C):
template <class ElementType> class Stack : private Array<ElementType> { public: using Array<ElementType>::Count;
void Push(const ElementType& elem);
const ElementType& Top(void) const;
ElementType Pop(void); };
Note first that the class itself defines no private data, and in fact consists entirely of specifications of the stack's interface¤.
The first of those specifications is rather unusual. A using
declaration in a class - see (TDCS, § 7.3.3.) -
causes the accessibility of the specified name (in this case,
Count) to be restored to the current accessibility (in this case, public).
Note that the using directive requires only the name of the
member function, rather than its entire prototype.
This "publicizing" approach can be used in this case because the
semantics of counting the elements of a stack are identical to those
of counting an array. Some (most?) compilers do not yet implement the
using directive, in which case we would either have to use the
deprecated "access specification" syntax
- see (TDCS, § 11.3):
template <class ElementType> class Stack : private Array<ElementType> { public: Array<ElementType>::Count; // CHANGE ACCESS BACK TO public
void Push(const ElementType& elem);
const ElementType& Top(void) const;
ElementType Pop(void); };
or provide a simple member function to forward the count request:
template <class ElementType> class Stack : private Array<ElementType> { public: int Count(void);
void Push(const ElementType& elem);
const ElementType& Top(void) const;
ElementType Pop(void); };
template <class ElementType> int Stack<ElementType>::Count(void) const { return Array<ElementType>::Count(); }
The other public member functions are extremely straightforward.
Pushing an element into a stack is equivalent to appending it to an
array. Hence Stack::Push does exactly that, taking advantage of the
(now private) Append functionality it inherits from Array:
template <class ElementType> void Stack<ElementType>::Push(const ElementType& elem) { Append(elem); }
Retrieving the value of the top element of the stack is the same as
accessing the last element of an array, the index of which is one less
than the number of items in the array (since Arrays are indexed
from 0):
template <class ElementType> const ElementType& Stack<ElementType>::Top(const ElementType& elem) const { return operator[]( Count()-1 ); }
Popping a stack is the same as retrieving the last element of an
array and then removing that element from the array (note the reuse of
Stack::Top()):
template <class ElementType> ElementType Stack<ElementType>::Pop(const ElementType& elem) { ElementType top = Top(); RemoveLast(); return top; }
In other words, each member function in the wrapper class's interface¤ is defined by specifying the equivalent operation(s) on the pre-existing class which is being wrapped.
See also: Exercise 1
Of course, it is not necessary to wrap an existing class in order to
make use of its implementation¤. The Stack class could just as easily
be implemented by encapsulating the necessary Array component:
template <class ElementType> class Stack { public: void Push(const ElementType& elem);
const ElementType& Top(void) const;
ElementType Pop(void);
int Count(void);
private: Array<ElementType> myArray; };
template <class ElementType> void Stack<ElementType>::Push(const ElementType& elem) { myArray.Append(elem); }
template <class ElementType> const ElementType& Stack<ElementType>::Top(const ElementType& elem) const { return myArray[ Count()-1 ]; }
template <class ElementType> ElementType Stack<ElementType>::Pop(const ElementType& elem) { ElementType top = Top(); myArray.RemoveLast(); return top; }
template <class ElementType> int Stack<ElementType>::Count(void) const { return myArray.Count(); }
In many respects the choice of where to inherit or encapsulare is one of style, as there is often no clear advantage either way. Some people feel the inheritance approach is a little cleaner but others contend that the encapsulation¤ approach is clearer and more straight-forward.
An obvious case in point is the two different approaches to providing
a Count() member function. The inheritance approach solves the
problem by "publicizing" the privately inherited Array::Count() member
function with a using declaration. The encapsulation¤ approach
declares a new function which forwards the count request to the
encapsulated Array object.
In this particular case both alternatives may be criticized in terms of
code performance and/or maintainability. The using declaration is an
unfamiliar mechanism to many C++ programmers and the syntax does not
make it obvious that a member function is being declared. On the other
hand, the encapsulation¤ approach adds the overhead of an extra function
call (which might be inline'd away).
One situation in which the inheritance-based approach as a clear advantage is the (admittedly unusual) case where the class being wrapped (that is, the class being inherited from) provides virtual functions that the wrapper class may wish to overload.
For example, if the Array class had a virtual Debug()
function (which might be called automatically when errors are detected
in one of Array's member functions), the Stack class might wish to
provide its own version of Debug(), so that extra information -
such as the fact that the Array being debugged is really part of
a Stack - could be passed.
Another case where inheritance is clearly preferable is where the class being wrapped provides protected members, and - usually for efficiency - it is necessary that the wrapper class has access to these members.
For example, the Array::RemoveLast() member might have been declared
protected, on the grounds that it is really concerned with the
internals of the Array class. In that case, encapsulating an Array in
a Stack would prevent us from calling Array::RemoveLast() to
manage the size of the stack. This might be a significant drawback as
stacks are typically much more volatile¤ in size than arrays.
See also: Exercise 2
See also: Exercise 3
Last updated: Fri Feb 18 11:17:30 2000