More Books
Effective C++ 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Table of Contents
Copyright
Praise for Effective C++, Third Edition
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Introduction
Terminology
Chapter 1. Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
Item 2: Prefer consts, enums, and inlines to #defines
Item 3: Use const whenever possible
Item 4: Make sure that objects are initialized before they're used
Chapter 2. Constructors, Destructors, and Assignment Operators
Item 5: Know what functions C++ silently writes and calls
Item 6: Explicitly disallow the use of compiler-generated functions you do not want
Item 7: Declare destructors virtual in polymorphic base classes
Item 8: Prevent exceptions from leaving destructors
Item 9: Never call virtual functions during construction or destruction
Item 10: Have assignment operators return a reference to *this
Item 11: Handle assignment to self in operator=
Item 12: Copy all parts of an object
Chapter 3. Resource Management
Item 13: Use objects to manage resources.
Item 14: Think carefully about copying behavior in resource-managing classes.
Item 15: Provide access to raw resources in resource-managing classes.
Item 16: Use the same form in corresponding uses of new and delete.
Item 17: Store newed objects in smart pointers in standalone statements.
Chapter 4. Designs and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
Item 19: Treat class design as type design
Item 20: Prefer pass-by-reference-to-const to pass-by-value
Item 21: Don't try to return a reference when you must return an object
Item 22: Declare data members private
Item 23: Prefer non-member non-friend functions to member functions
Item 24: Declare non-member functions when type conversions should apply to all parameters
Item 25: Consider support for a non-throwing swap
Chapter 5. Implementations
Item 26: Postpone variable definitions as long as possible.
Item 27: Minimize casting.
Item 28: Avoid returning "handles" to object internals.
Item29: Strive for exception-safe code.
Item 30: Understand the ins and outs of inlining.
Item31: Minimize compilation dependencies between files.
Chapter 6. Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models "is-a."
Item 33: Avoid hiding inherited names
Item 34: Differentiate between inheritance of interface and inheritance of implementation
Item 35: Consider alternatives to virtual functions
Item 36: Never redefine an inherited non-virtual function
Item 37: Never redefine a function's inherited default parameter value
Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition
Item 39: Use private inheritance judiciously
Item 40: Use multiple inheritance judiciously
Chapter 7. Templates and Generic Programming
Item 41: Understand implicit interfaces and compile-time polymorphism
Item 42: Understand the two meanings of typename
Item 43: Know how to access names in templatized base classes
Item 44: Factor parameter-independent code out of templates
Item 45: Use member function templates to accept "all compatible types."
Item 46: Define non-member functions inside templates when type conversions are desired
Item 47: Use traits classes for information about types
Item 48: Be aware of template metaprogramming
Chapter 8. Customizing new and delete
Item 49: Understand the behavior of the new-handler
Item 50: Understand when it makes sense to replace new and delete
Item 51: Adhere to convention when writing new and delete
Item 52: Write placement delete if you write placement new
Chapter 9. Miscellany
Item 53: Pay attention to compiler warnings.
Item 54: Familiarize yourself with the standard library, including TR1
Item.55: Familiarize yourself with Boost.
Appendix A. Beyond Effective C++
Appendix B. Item Mappings Between Second and Third Editions
Index
index_SYMBOL
index_A
index_B
index_C
index_D
index_E
index_F
index_G
index_H
index_I
index_J
index_K
index_L
index_M
index_N
index_O
index_P
index_R
index_S
index_T
index_U
index_V
index_W
index_X
index_Z

Item 41: Understand implicit interfaces and compile-time polymorphism

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:

  • Because w is declared to be of type Widget, w must support the Widget interface. We can look up this interface in the source code (e.g., the .h file for Widget) to see exactly what it looks like, so I call this an explicit interface — one explicitly visible in the source code.

  • Because some of Widget's member functions are virtual, 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 (see Item 37).

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?

  • The interface that w must support is determined by the operations performed on w in the template. In this example, it appears that w's type (T) must support the size, normalize, and swap member functions; copy construction (to create temp); and comparison for inequality (for comparison with someNastyWidget). We'll soon see that this isn't quite accurate, but it's true enough for now. What's important is that the set of expressions that must be valid in order for the template to compile is the implicit interface that T must support.

  • The calls to functions involving w such as operator> and operator!= may involve instantiating templates to make these calls succeed. Such instantiation occurs during compilation. Because instantiating function templates with different template parameters leads to different functions being called, this is known as compile-time polymorphism.

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:

  • It must offer a member function named size that returns an integral value.

  • It must support an operator!= function that compares two objects of type T. (Here, we assume that someNastyWidget is of type T.)

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

  • 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.