Effective C++ item 41: Understand implicit interfaces and compile-time polymorphism

People are mostly familar with explicit interface, let’s use an example

1
2
3
4
5
6
void doProcessing(Widget& w) {
if (w.size() > 5) {
w.normalize();
w.swap(w);
}
}

What we can say about w is that it must support the Widget interface. We can look up this interface in the sourse code of Widget to see what it looks like. This is explicit interface - one explicitly visible in the source code.

In above case, because Widget can be a base class with virtual functions, w‘s calls to those functions will exhibit runtime polymorphism: the specific function to call will be determined at runtime based on w‘s dynamic type.

As you would imagine, we can pass objects of following classes to above function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget {
public:
Widget();
virtual ~Widget();
virtual size_t size() const;
virtual void normalize();
void swap(Widget& other);
};

class NastyWidget : public Widget {
public:
NastyWidget();
virtual ~NastyWidget();
size_t size() const;
void normalize();
};

The world of templates and generic programming is fundamentally different. In that world, explicit interfaces and runtime polymorphism continue to exist, but they’re less important. Instead, implicit interfaces and compile-time polymorphism move to the fore. To see how this is the case, look what happens when we turn doProcessing from a function into a function template:

1
2
3
4
5
6
7
template<typename T>
void doProcessingTP(T& w)
{
if (w.size() > 5) {
...
}
}

The implicit interface for T (w‘s type) appears to have these constraints:

  • It must offer a member function named size
  • There must be a operator> function that compares two objects, the one returned by w.size() and int.

Pay attention to the wording, I didn’t say T must has a size function that returns integral type. That’s what you will expect for explicit interface without template. Implicit interfaces are simply made up of a set of valid expressions, which exist only in developer’s head. The expressions themselves may look complicated, but the constraints they impose are generally straightforward.

So what object can I pass to doProcessingTP? One of made up example is

1
2
3
4
5
6
7
8
9
10
11
12
class RandomType {
public:
RandomType();
virtual ~RandomType();
RandomType size() const;
...
};

bool operator>(RandomType, int)
{
return true;
}

I intentionally make size not return std::size_t, but an arbitrary type, RandomType itself. And as long as I provide a comparison function which compares RandomType and int, I can pass an instance of RandomType to doProcessingTP.

If you tries to pass the same object to following function, you will get compilation error

1
2
3
4
5
void doProcessing(RandomType& w) {
if (w.size() > 5) {
...
}
}

The error will be invalid operands to binary expression ('RandomType' and 'int'). Because there is no implicit convertion from RandomType to int.

Things to remember:

  • Both classes and templates support interfaces and polymorphism.
  • For classes, interfaces are explicit and centered on function signatures. Polymorphism occurs at runtime through virtual functions.
  • For template parameters, interfaces are implicit and based on valid expressions. Polymorphism occurs during compilation through template instantiation and function overloading resolution.

Reference:
“Effective C++” Third Edition by Scott Meyers.