Conditional Interfaces:

Changing Container Interfaces Based on Contained Type

Ali Rahimi (last modified May 27, 2000)


1.0 The Problem

On occasion, I've found it useful to be able to change the interface of a container based on the type of the object contained. For example, suppose I define a container called Vector, which provides an add() method:

Vector<T> {
  ...
  Vector<T> add(Vector<T> a) {
    Vector<T> sum;
    forall(a) {
      sum[i]=add(a[i],self[i])
    }
    return sum
  }
  ...
}

The Vector::add() method imposes a constraint on the contained type: T must implement an add() method for Vector::add() to be well-formed.

Now suppose that 90% of the methods in Vector don't actually require the T::add() method to be implemented and that the contained type T does in fact not implement add(). Is there any hope of creating a vector of T's? There are several trivial solutions to the problem, and as far as I can tell, none of them are suitable, so I resort to conditional interfaces.

2.0 Typical Answers

2.1 Many Classes

Let's first explore the simple solution of creating several versions of Vector which impose different requirements on the contained type. For example, we could write a Vector_without_add<T> which implements all of Vector<T> except for the add() method. This is trivially accomplished using inheritance:

Vector_without_add<T>
{
  // all the vector methods, but add().
}
Vector<T> : inherit Vector_without_add<T>
{
   Vector<T> add(Vector<T>);
}

Unfortunately, using inheritance as a code reuse mechanism doesn't help at all in the more general case when the contained type can influence the interface of the container in more than one way.

For example, using inheritance how do you deal with a Vector::mult() method which must invoke T::mult()? Certainly, we could create a Vector_simple without either add() or multiply(), then derive Vector_with_add and Vector_with_mult, AND Vector_with_add_and_mult:

Vector_simple<T>
{
  // basic vector operations, without add and mult.
}
Vector_with_add<T> : inherit Vector_simple<T>
{
   Vector<T> add(Vector<T>);
}
Vector_with_mult<T> : inherit Vector_simple<T>
{
   Vector<T> mult(Vector<T>);
}
Vector_with_mult_and_add<T> : inherit Vector_simple<T>
{
   Vector<T> add(Vector<T>);
   Vector<T> mult(Vector<T>);
}

This works for two constraints, but the number of classes we have to define now grows exponentially with the number of switchable features. Besides, we've required the user to wade through many classes and pick the right combination of keywords. Inheritance is clearly not an acceptable code reuse tool for this situation, so we have to try something else.

2.2 C++-style Templates

We could go the C++ template way, where using Vector<T_without_add> generates errors only if Vector<T_without_add>::add() is called. You get compile-time error which points somewhere in the middle of Vector::add() (specifically, at the line sum[i]=add(a[i],self[i]) above).

This solution is nice because you don't get a runtime error, but it's a pain in every other way because template compile-time bugs are REALLY difficult to track down.

2.3 Testing Interfaces at Runtime

We could require that T ALWAYS provide an add() method, even if it's a stub that throws a NotImplemented exception.

This solution is both lame and stupid because by the time the programmer realizes that the Vector::add() method doesn't work, he's already compiled and run the program, and wasted a significant amount of design, programming, and debugging time. Even more insidious, since there is no compile time checking, it might be years before the call to Vector::add() is made, and the program crashes on the user's lap. Deeply lobotomized monkeys seem to like this technique however.

Throwing NotImplemented is much worse than not implementing the method, as we are converting a statically checkable problem into a run-time check. If we go for that kind of solution, we might as well put our coats back on and program in lisp.

3.0 Conditional Interfaces

If the interface of the contained class can be querried, then we can use multiple inheritance to implement conditional interfaces in C++. This is not the ultimate solution to the problem, but it's the reason why I'm writing this note, so I'm dedicating a large section to it.

3.1 Conditional Inheritance

We can declare a C++ class Container which derives from class Base1 or Base2 depending on some condition:

class Container : public META_IF<CONDITION, Base1, Base2>::V
{ ...
}

where META_IF is a cleverly constructed template class which uses partial specialization to select between its second and third template arguments:

template <bool CONDITION, class TRUE_TYPE, class FALSE_TYPE>
struct META_IF {};

template <class TRUE_TYPE, class FALSE_TYPE>
struct META_IF<false, TRUE_TYPE, FALSE_TYPE> { typedef FALSE_TYPE V; };

template <class TRUE_TYPE, class FALSE_TYPE>
struct META_IF<true, TRUE_TYPE, FALSE_TYPE> { typedef TRUE_TYPE V; };

Equipped with this concept and multiple inheritance, it's pretty easy to implement a conditional interface.

3.2 Conditionally Inheriting from Multiple Forwarders

Since publicly inheriting from a class implies implementing its interface, the above section gives us a mechanism for modifying the interface of Container based on a condition. As a first step to solving our problem, let's assume we conditionally inherit from forwarder classes:

template 
class Container : public META_IF<T::HasAdd, add_forwarder, unimplemented>::V,
                  public META_IF<T::HasMult, mult_forwarder, unimplemented>::V
{
   ...
private:
   virtual Container<T> _add(const Container<T> &) { /* implementation of add. */ }
   virtual Container<T> _mult(const Container<T> &) { /* implementation of mult. */ }
}

where a typical forwarder looks like:

class mult_fowarder
{
public:
template <class CONTAINER>  mult(const CONTAINER &c) {
         return _mult(c);
 }
private:
template <class CONTAINER> virtual CONTAINER _mult(const CONTAINER &c) = 0;
}

If T::HasAdd is true, a call to Container::add() is actually a call to mult_forwarder::add(), which forwards the call to Container::_add() by calling the pure virtual function mult_forwarder::_add().

If T::HasAdd is false, Container inherits from unimplemented, which does not implement any methods, and hence add() does not appear in Container (although _add() is still there).

This simple example which illustrates the concept, using the COND_INTERFACE construct.

4.0 More Examples

5.0 Reviews

 yaroslav> so you may have just discovered a use for multiple inheritance.