Programming Interfaces – Implicit vs Explicit Interfaces

duck-typingexplicitinterfacespolymorphismtemplates

I think I understand the actual limitations of compile-time polymorphism and run-time polymorphism. But what are the conceptual differences between explicit interfaces (run-time polymorphism. ie virtual functions and pointers/references) and implicit interfaces (compile-time polymorphism. ie. templates).

My thoughts are that two objects that offer the same explicit interface must be the same type of object (or have a common ancestor), while two objects that offer the same implicit interface need not be the same type of object, and, excluding the implicit interface that they both offer, can have quite different functionality.

Any thoughts on this?

And if two objects offer the same implicit interface, what reasons (beside the technical benefit of not needing dynamic dispatch w/ a virtual function lookup table, etc) are there for not having these objects inherit from a base object that declares that interface, thus making it an explicit interface? Another way of saying it: can you give me a case where two objects that offer the same implicit interface (and therefore can be used as types to the sample template class) should not inherit from a base class that makes that interface explicit?

Some related posts:


Here's an example to make this question more concrete:

Implicit Interface:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Explicit Interface:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

An even more in-depth, concrete example:

Some C++ problems can be solved with either:

  1. a templated class whose template type provides an implicit interface
  2. a non-templated class that takes a base-class pointer which provides an explicit interface

Code that doesn't change:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Case 1. A non-templated class that takes a base-class pointer which provides an explicit interface:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Case 2. A templated class whose template type provides an implicit interface:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Case 3. A templated class whose template type provides an implicit interface (this time, not deriving from CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Case 1 requires that the object being passed in to useCoolClass() be a child of CoolClass (and implement worthless()). Cases 2 and 3, on the other hand, will take any class that has a doSomethingCool() function.

If users of the code were always fine subclassing CoolClass, then Case 1 makes intuitive sense, since the CoolClassUser would always be expecting an implementation of a CoolClass. But assume this code will be part of an API framework, so I cannot predict if users will want to subclass CoolClass or roll their own class that has a doSomethingCool() function.

Best Answer

You've already defined the important point- one is run-time and the other is compile-time. The real information you need is the ramifications of this choice.

Compiletime:

  • Pro: Compile-time interfaces are much more granular than run-time ones. By that, what I mean is that you can use only the requirements of a single function, or a set of functions, as you call them. You don't have to always do the whole interface. The requirements are only and exactly what you need.
  • Pro: Techniques like CRTP mean that you can use implicit interfaces to default implementations of things like operators. You could never do such a thing with run-time inheritance.
  • Pro: Implicit interfaces are much easier to compose and multiply "inherit" than run-time interfaces, and don't impose any kind of binary restrictions- for example, POD classes can use implicit interfaces. There's no need for virtual inheritance or other shenanigans with implicit interfaces- a big advantage.
  • Pro: The compiler can do way more optimizations for compile-time interfaces. In addition, the extra type safety makes for safer code.
  • Pro: It's impossible to do value typing for run-time interfaces, because you don't know the size or alignment of the final object. This means that any case which needs/benefits from value typing gains big benefits from templates.
  • Con: Templates are a bitch to compile and use, and they can be fiddly porting between compilers
  • Con: Templates cannot be loaded at run-time (obviously), so they have limits in expressing dynamic data structures, for example.

Runtime:

  • Pro: The final type doesn't have to be decided until run-time. This means that run-time inheritance can express some data structures much easier, if templates can do it at all. Also you can export run-time polymorphic types across C boundaries, for example, COM.
  • Pro: It's much easier to specify and implement run-time inheritance, and you won't really get any compiler-specific behaviour.
  • Con: Run-time inheritance can be slower than compile-time inheritane.
  • Con: Run-time inheritance loses type information.
  • Con: Run-time inheritance is way less flexible.
  • Con: Multiple inheritance is a bitch.

Given the relative list, if you do not need a specific advantage of run-time inheritance, do not use it. It is slower, less flexible, and less safe than templates.

Edit: It's worth noting that in C++ particularly there are uses for inheritance other than run-time polymorphism. For example, you can inherit typedefs, or use it for type tagging, or use the CRTP. Ultimately, though, these techniques (and others) really fall under "Compile-time", even though they are implemented using class X : public Y.

Related Topic