Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
The world of object-oriented programming revolves around explicit interfaces and runtime polymorphism. For example, given this (meaningless) class, class Widget { public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); // see Item 25 ... };
and this (equally meaningless) function, void doProcessing(Widget& w) { if (w.size() > 10 && w != someNastyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
we can say this about w in doProcessing:
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: template<typename T> void doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { T temp(w); temp.normalize(); temp.swap(w); } }
Now what can we say about w in doProcessing?
Even if you've never used templates, you should be familiar with the difference between runtime and compile-time polymorphism, because it's similar to the difference between the process of determining which of a set of overloaded functions should be called (which takes place during compilation) and dynamic binding of virtual function calls (which takes place at runtime). The difference between explicit and implicit interfaces is new to templates, however, and it bears closer examination. An explicit interface typically consists of function signatures, i.e., function names, parameter types, return types, etc. The Widget class public interface, for example, class Widget { public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); }; consists of a constructor, a destructor, and the functions size, normalize, and swap, along with the parameter types, return types, and constnesses of these functions. (It also includes the compiler-generated copy constructor and copy assignment operator see Item 5.) It could also include typedefs and, if you were so bold as to violate Item 22's advice to make data members private, data members, though in this case, it does not. An implicit interface is quite different. It is not based on function signatures. Rather, it consists of valid expressions. Look again at the conditional at the beginning of the doProcessing template: template<typename T> void doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { ... The implicit interface for T (w's type) appears to have these constraints:
Thanks to the possibility of operator overloading, neither of these constraints need be satisfied. Yes, T must support a size member function, though it's worth mentioning that the function might be inherited from a base class. But this member function need not return an integral type. It need not even return a numeric type. For that matter, it need not even return a type for which operator> is defined! All it needs to do is return an object of some type X such that there is an operator> that can be called with an object of type X and an int (because 10 is of type int). The operator> need not take a parameter of type X, because it could take a parameter of type Y, and that would be okay as long as there were an implicit conversion from objects of type X to objects of type Y! Similarly, there is no requirement that T support operator!=, because it would be just as acceptable for operator!= to take one object of type X and one object of type Y. As long as T can be converted to X and someNastyWidget's type can be converted to Y, the call to operator!= would be valid. (As an aside, this analysis doesn't take into account the possibility that operator&& could be overloaded, thus changing the meaning of the above expression from a conjunction to something potentially quite different.) Most people's heads hurt when they first start thinking about implicit interfaces this way, but there's really no need for aspirin. Implicit interfaces are simply made up of a set of valid expressions. The expressions themselves may look complicated, but the constraints they impose are generally straightforward. For example, given the conditional, if (w.size() > 10 && w != someNastyWidget) ...
it's hard to say much about the constraints on the functions size, operator>, operator&&, or operator!=, but it's easy to identify the constraint on the expression as a whole. The conditional part of an if statement must be a boolean expression, so regardless of the exact types involved, whatever "w.size() > 10 && w != someNastyWidget" yields, it must be compatible with bool. This is part of the implicit interface the template doProcessing imposes on its type parameter T. The rest of the interface required by doProcessing is that calls to the copy constructor, to normalize, and to swap must be valid for objects of type T. The implicit interfaces imposed on a template's parameters are just as real as the explicit interfaces imposed on a class's objects, and both are checked during compilation. Just as you can't use an object in a way contradictory to the explicit interface its class offers (the code won't compile), you can't try to use an object in a template unless that object supports the implicit interface the template requires (again, the code won't compile). Things to Remember
|