[content]
Item 41: Understand implicit interfaces and compile time polymorphism
Both class and templates support interfaces and polymorphism.
For classes, interfaces are explicit and centered on function signatures. And polymorphism occurs at runtime through virtual functions.
For templates, interfaces are implicit and based on valid expressions. And polymorphism occurs during compilation through template instantiation and function overloading.
Item 42: Understand the two meanings of typename
DO: When declaring template parameters, class and typename are interchangeable.
DO: Typename is needed when identifying nested dependent type names.
template<typename IterT>
void WorkWithIterator(IterT iter) {
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
Item 43: Know how to access names in template base classes
DO: In derived class templates, refer to names in base class templates via a “this->” prefix, via using declarations, or via explicit base class qualification.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void SendClearMsg(const MsgInfo& info) {
SendClear(info); // call base function; this code will not compile!
}
}
WHY: When compilers encounter the definition for the class, they don’t know what class it inherits from. The base class is MsgSender<Company>. Compilers cannot pre-determine if all instantiations of the template base class have the name (including the all kinds of specialization of the template base class). Therefore, compilers generally refuse to “look in template base class”.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
// Approach 1
void SendClear(const MsgInfo& info) {
this->SendClear(info);
}
// Approach 2
using MsgSender<Company>::SendClear;
void SendClearMsg(const MsgInfo& info) {
SendClear(info);
}
// Approach 3
void SendClearMsg(const MsgInfo& info) {
MegSender<Company>::SendClear(info);
}
}
Item 44: Factor parameter-independent code out of templates
WHY: Templates generate multiple classes and multiple functions, any template codes not dependent on a template parameter causes bloat.
DO: Bloat due to non-type template parameters can often be eliminated by replacing template parameters with function parameters or class members.
// For each different n, there will be an instantiation for this class.
template<typename T, std::size_t n>
class SquareMatrix {
public:
void invert();
}
Example: move non-type template parameter as a function parameter.
// Create a size-independent base class
template<typename T>
class SquareMatrixBase {
protected:
void invert(std::size_t matrix_size);
private:
T *p_data; // pass in the data pointer to operate on.
}
// All template derived class will share the same copy of
// SquareMatrixBase<T>
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> { // private inheritance: "implemented-in-terms-of"
private:
using SquareMatrixBase<T>::invert;
public:
// Now we don't need to instantiate this function for each different n.
void invert() { invert(n);}
}
DO: Bloat due to type template parameters can be reduced by sharing implementations for instantiation types with identical binary representations. Typically, this means implementing member functions that work with strongly typed pointers by having them call functions that work with untyped pointers (i.e. void* pointers). For example, the implementation for vector, deque and list.
Item 45: Use member function templates to accept “all compatible types”
DO: use member template function to accept all compatible types, because compliers view each instantiation for the template as a completely different class.
template<typename T>
class SmartPointer {
public:
// template constructor function, construct SmartPointer<T> with any compatible SmartPointer<U>
template<typename U>
SmartPointer(const SmartPointer<U>& other);
}
DO: If you declare template copy constructor or copy assignment constructor, you need to declare normal copy constructor and copy assignment too (non-template version).
Item46: Define non-member function when type conversion is needed
DO: Define non-member function when we need to support implicit type conversions on all parameters. (same as non-template class) But for template class, we need to define those functions as friends in the template class too.
// The template class
template<typename T>
class Rational {
public:
Rational(const T& numerator, const T& denominator) {...};
};
// Declare the non-member function
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {...}
Rational<int> result = one_half * 2; // error! won't compile. But in the non-template class situation, this will compile.
WHY: The type deduction is different for implicit conversion in non-template classes and template classes.
1. Compilers try to figure out what function to instantiate from the template function named operator*.
2. To deduce T, compilers check types that are passed in. Left side is Rational<int>, right side is int. Each parameter is considered separately!
3. The deduction for the left side is easy, T is int.
4. How are compilers to figure out T, when they have Rational<T> and int? The implicit type conversion via constructor calls is not considered during template argument deduction.
// Declare the non-member helper function.
template<typename T>
const Rational<T> DoMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
// The template class
template<typename T>
class Rational {
public:
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
// Let the "inline" friend function call the helper function to avoid bloat code.
return DoMultiply(lhs, rhs);
}
};
// The definition of the helper function.
template<typename T>
const Rational<T> DoMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {...}
WHY: If we have a friend function inside the template class, when the template class is instantiated, the friend function will also be declared too. Now, as a declared function (not a template function), compilers can use implicit conversion from int to Rational<int>.
Item47: Use traits classes for information about types
DO: In conjunction with overloading, traits class make it possible to perform compile-time if…else tests on your types.
Example: the implementation of advance.
There are five types of iterator:
1. Input iterators: Move forward only, one step at a time, read once. (modeled on the read pointer of a file)
2. Output iterators: Move forward only, one step at a time, write once. (modeled on the writer pointer to a file)
3. Forward iterators: Move forward only, one step at a time, read & write multiple time
4. Bidirectional iterators: Move forward and backward, one step at a time, read & write multiple time
5. Random access iterators: in additional to bidirectional iterators, support iterator arithmetic
// The following are "trait classes"
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};
DO: To store information for types, there are two steps
1. Enforce each type to typedef a name with different trait class in their definition
2. Sine we cannot typedef information into built-in type, we need to use another template class and template specialization to support built-in type. (If we only use user-defined types, step 2 is not necessary)
// A template class to wrap all user-defined iterator classes.
template<typename IterT>
struct iterator_traits {
// IterT::iterator_category are different for different IterT
typedef typename IterT::iterator_category iterator_category;
};
// Partial template specialization for built-in iterator type (C++ pointer)
template<typename T>
struct iterator_traits<T*> {
typedef random_access_iterator_tag iterator_category;
};
DO: use function overload to perform “if-else” in compile time.
// Define DoAdvance for each iterator tag
template<typename IterT, typename DistT>
void DoAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {...}
// Call overloaded function
template<typename IterT, typename DistT>
void Advance(IterT& iter, DistT d) {
DoAdvance(iter, d, typename iterator_traits<IterT>::iterator_category);
}
Item48: Be aware of template meta-programming
Template meta programming (TMP) is the process of writing template based C++ programs that execute during compilation.
WHY: Advantages: TMP could make some implementation easy that would be otherwise hard or impossible. TMP moves work from runtime to compile-time, which could help detect errors earlier. TMP can be more efficient: smaller executables, shorter runtime, less memory usage. Disadvantages: more compile resource requirement, cognitive cost.
DO: TMP is Turing-complete, which means it is able to compute anything. Example: using recursive template instantiation to perform a loop.
// General case.
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value, };
}
// Base case.
template<>
struct Factorial<0> {
enum { value = 1, };
}
Other examples of TMP:
1. Ensuring dimensional unit correctness. E.g. unit^(1/2) is the same as unit^(4/8)
2. Optimizing matrix operations. E.g. Merge multiplier in Matrix res = m1*m2*m3;
3. Generating custom design pattern implementations. E.g. policy-based design