Interaction through delegation

Instructor's Guide


intro, components, case study, crossing boundaries, styles, platform, summary, Q/A, literature
The handler/body idiom (as introduced in Coplien, 1992, see section canonical is an elegant way in which to separate the implementation of a concrete data type (the body) from its interface (the handler). In the previous section, we looked at an example employing explicit weak delegation (forwarding) to redirect messages to the actual realization of some type. In this section, we will look at how we may use implicit (weak) delegation in C++, and as a related issue, how we may define safe or smart pointers in C++. Next, we will look at the use of delegation to regulate the processing of elements of various types within a single heterogeneous structure (employing tags). Finally, we will consider how we may implement dynamic types (allowing for multiple roles) using a combination of inheritance and (implicit) delegation.

Implicit delegation {\em -- smart pointers}

Operator overloading is one of the most powerful features offered by C++. In particular, the possibility of overloading the de-reference operator offers vast opportunities for employing idioms deviating from the classical object-based approach, and (needless to say) for abuse. An interesting application of overloading the de-reference operator is the definition of smart pointers employing implicit forwarding. As an example, assume that we have a class F defining functional behavior of some kind, as in slide 7-del-F.

Functional behavior

  class F { 
\fbox{F}
public: void a() { cout << "a"; } void b() { cout << "b"; } };

slide: Functional behavior

Instead of accessing instances of F directly, we may define an interface class, say A and delegate the requests addressed to an instance of A to an instance of F embedded in the instance of A, as depicted in slide 7-del-A.

Interface -- access

  class A { 
\fbox{A}
public: A() { delegate = new F; } F* operator->() { return delegate; } void extra() { cout << "extra"; } private: F* delegate; };

slide: An interface class

The interface class A also offers, apart from a constructor and the definition of operator->, a member function extra (that is merely introduced to illustrate the difference between invoking a method for A and using an instance of A to delegate a messages to the encapsulated instance of F).
  A o; o.extra(); o->a(); o->b();
  
In the program fragment above, an object instance o of A is created, the method extra is invoked, and then o is used as if it were a pointer to an instance of F. The exact workings of de-referencing may seem a little tricky. For technical details the reader is referred to  [ES90].

Smart pointers

-- OMDG standard \zline{\fbox{Ref}}
  template< class T >
  class Ref { 
  public:
  
  Ref(const T*); 
constructors

Ref(const Ref&); T* operator*() const;
\ifsli{deref}{de-reference operators}

T* operator->() const; operator T*() const;
type conversion
Ref& operator=(const T*);
assignment
Ref& operator=(const Ref< T >&); };

slide: Smart pointers

Smart pointers

Overloading the de-reference operator may be easily abused. Nevertheless, it is a powerful mechanism that may be used to implement smart (that is safe and flexible) pointers. Such pointers have been proposed as part of the Object Database Management Group (ODMG) standard C++ interface for object databases. See  [Strick93]. The interface proposed for such pointers is shown in slide 7-smart. The interface for the template class Ref contains constructors, de-reference operators, a type conversion operator (to obtain a pointer to an instance of T) and assignment operators. The reader is invited to develop an implementation for this class. Instances of Ref are used to access objects stored in the database. After retrieving objects from persistent storage (the object data base) into memory, ordinary pointers (obtained by one of the de-reference or type conversion operators) may be used as well. As described in  [Strick93], additional data types (such as sets and lists) are needed to define a flexible interface to the object data base. In addition, a query language (\`{a} la SQL) may be needed to access the database.

Regulation {\em -- heterogeneous types}

Many applications require heterogeneous structures, such as, for example, lists to store integers and strings (read from standard input) that must be processed afterwards. For the application programmer it is convenient in such cases to have a single consistent interface, hiding the actual realization. The handler/body idiom (employing implicit delegation) is well-suited to this. In the following, we will look at an example of a heterogeneous structure (employing implicit delegation). Again, we distinguish between an abstract interface class and its realization(s). To avoid cluttering the code, we employ a general (abstract) realization class and refine this to obtain the actual element classes. The interface class for our container may look as depicted in slide 7-reg-A.
  class R; 
forward reference

class A {
\fbox{A}
public: A(int n);
integer elements

A(char* s);
char* elements

~A() { delete rep; } R* operator->() { return rep; } private: R* rep; };

slide: The interface class

It contains two constructors, one that accepts an integer and one that takes a string. In addition, it provides a destructor and an overloaded de-reference operator, allowing implicit delegation. The general (abstract) realization type may be defined as in slide 7-reg-R.
  class R { 
R
public: R() { next = 0; } virtual ~R() { if (next) delete next; } void insert(A& r); void process() { action(); if (next) (*next)->process(); } virtual void action() = 0; protected: A* next; }; R::insert(A& r) {
needs some} trickery
if (!next) next = &r; else (*next)->insert(r); }

slide: Abstract realization

The class R contains the functionality that is shared by each of the classes corresponding to the actual element types. Note that the function action is assigned zero, making R an abstract class. It is used by the function process to perform the action required for an element. The functions insert may be defined in a straightforward way. However, some trickery is needed to allow insert to be applied to the next pointer. Remember that the de-reference operator applies to objects of type A and delivers a pointer to an R object. Since R is an abstract class, the destructor for R must be declared virtual. The function process amounts to printing the value of the elements (as defined by the function action of the element class). As an example of two realization types look at the classes N and S in slide 7-reg-C.

Concrete realizations

  class N : public R {  
\fbox{N}
public: N(int i) : n(i) { } void action() { cout << n << endl; } protected: int n; }; A::A(int n) { rep = new N(n); }
// constructor for A
  class S : public R {  
\fbox{S}
public: S(char* p) : s(p) { } void action() { cout << s << endl; } protected: char* s; }; A::A(char* s) { rep = new S(s); }
// constructor for A

slide: Concrete realizations

Note that the creation of actual instances of N and S is hidden in the constructors of A. This technique is characterized in  [Coplien92] as the virtual constructor idiom, since the actual type of the body instance of A is determined dynamically. Since these are simple types, the destructor may safely be omitted. As an example of using the heterogeneous container type A, look at the following fragment. First, an (integer) element is created (which becomes the first element of the chain) and then elements are appended to the chain
  A e1(2),e2("hi"),e3(1);
  e1->insert(e2); e1->insert(e3);
  e1->process();
  
In the solution presented we rely on the implicit dispatching of the virtual function mechanism to process the elements according to their types. An advantage of using classes (instead of tag fields) to define the various element types is that the collection of element types can be easily extended. Nevertheless, when adding a new element type the collection of constructors for the abstract interface A must still be extended.

Dynamic typing {\em -- multiple roles}

An often heard criticism of statically typed object-oriented languages is that they do not allow for the flexibility necessary to characterize objects that change their type dynamically. Actually, few dynamically typed languages allow for changing the type of an object as well. However, the scheme employed to implement heterogeneous container types may be easily extended to allow for changing types (or roles) dynamically as well. The construction relies (again) on the definition of an abstract interface comprising the interfaces of the (role) realization classes. In addition, we need a control class (containing a tag field) to switch between (realization) types dynamically. Consider the abstract interface class A, shown in slide 7-rol-A.
  class A { 
\fbox{A}
public: virtual void talk() { cout << "*talk*"; } virtual void think() { cout << "*think*"; } virtual void act() { cout << "*act*"; } };

slide: Abstract interface

The class A provides a so-called fat interface, combining all possible behaviors allowed by the role realization classes. Next, we need a control class C, which distinguishes between the roles of a PERSON and a STUDENT. See slide 7-rol-C.

Control

-- dispatching
  class C : public A { 
\fbox{C}
public: enum { PERSON = 0 , STUDENT }; C() { roles[PERSON]=0; roles[STUDENT]=0; role(0);
default role

} A* operator->() { return roles [role]; } void role( int r );
role switching

protected: int _role; A* roles[2]; };

slide: Control

Apart from a constructor and an overloaded de-reference operator, the class C contains a member function role (to switch between roles) and an array of roles (containing so to speak the repertoire of an instance of A). The default role is set to PERSON in the constructor. The realization classes P (for PERSON) and S (for STUDENT) give the actual definition for the virtual member functions declared in the interface class A. See slide 7-rol-R.
  class P : public A {  
\fbox{P}
public: void talk() { cout << "beach"; } void act() { cout << "..."; } };
  class S : public A { 
\fbox{S}
public: void talk() { cout << "OOP"; } void think() { cout << "Z"; } };

slide: Realizations

Note that each of the role (realization) classes allows only for a strict subset of the behavior allowed by the abstract interface class A. See slide 7-rol-switch.
  void C::role( int r ) { 
\fbox{C::role}
require( r == PERSON || r == STUDENT ); _role = r; if (roles [role] == 0) { switch (r) { case PERSON : roles [role] = new P(); break; case STUDENT : roles [role] = new S(); break; }; } }

slide: Switching between roles

Switching between roles employs the technique of virtual constructors, which is introduced in  [Coplien92] as a means to enhance the functionality of the envelope/letter idiom. For each possible role, an instance of the role (realization) class is created, to which requests addressed to the instance of C (figuring as a representative for each of the roles) are delegated. An example of using an instance of C is given below
  C s; 
  s.role(S::STUDENT);
  s->think(); s->talk(); s->act();
  s.role(S::PERSON);
  s->think(); s->talk(); s->act();
  
The disadvantage of the approach sketched here (and of fat interfaces in general) is that the compiler cannot ensure that only the functions relevant to a particular role are invoked. Obviously, the price that must be paid for flexibility of this kind is diminished compiler support and increased reliance on the programmer's discipline.