C++ Chapter 7. Templates and Generic Programming

1. 

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

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 onw in the template. In this example, it appears that w's type (T) must support thesizenormalize, and swap member functions; copy construction (to createtemp); 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> andoperator!= 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.


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

2. 

  • Until C is known, there's no way to know whether C::const_iterator is a type or isn't, and when the template print2nd is parsed,C isn't known. C++ has a rule to resolve this ambiguity: if the parser encounters a nested dependent name in a template, it assumes that the name is not a type unless you tell it otherwise. By default, nested dependent names (C::const_iterator, const_iterator is a type nested into class c which is depend on c.)are not types.(There is an exception to this rule that I'll get to in a moment.)

    With that in mind, look again at the beginning of print2nd:

    template<typename C>
    
    void print2nd(const C& container)
    
    {
    
      if (container.size() >= 2) {
    
         C::const_iterator iter(container.begin());   // this name is assumed to
    
         ...                                          // not be a type
    
    

    Now it should be clear why this isn't valid C++. The declaration of iter makes sense only if C::const_iterator is a type, but we haven't told C++ that it is, and C++ assumes that it's not. To rectify the situation, we have to tell C++ that C::const_iterator is a type. We do that by putting typename immediately in front of it:

    template<typename C>                           // this is valid C++
    
    void print2nd(const C& container)
    
    {
    
      if (container.size() >= 2) {
    
        typename C::const_iterator iter(container.begin());
    
        ...
    
      }
    
    }
    
    

    The exception to the "typename must precede nested dependent type names" rule is that typename must not precede nested dependent type names in a list of base classes or as a base class identifier in a member initialization list. For example:

    template<typename T>
    
    class Derived: public Base<T>::Nested { // base class list: typename not
    
    public:                                 // allowed
    
      explicit Derived(int x)
    
      : Base<T>::Nested(x)                  // base class identifier in mem
    
      {                                     // init. list: typename not allowed
    
    
    
        typename Base<T>::Nested temp;      // use of nested dependent type
    
        ...                                 // name not in a base class list or
    
      }                                     // as a base class identifier in a
    
      ...                                   // mem. init. list: typename required
    
    };
    
    

    3.
    when we use template in inheritance, compilers don't know what class it inherits from. Because a template parameter won't be known until later (when object of derived class is instantiated). Without knowing what template parameter is, there's no way to know what the base class looks like. So the inheritance will stop.
    
     
    template<>                                 // a total specialization of
    
    class MsgSender<CompanyZ> {                // MsgSender; the same as the
    
    public:                                    // general template, except
    
      ...                                      // sendCleartext is omitted
    
      void sendSecret(const MsgInfo& info)
    
      { ... }
    
    };
    
    

    4.

    template<typename Company>
    
    class LoggingMsgSender: public MsgSender<Company> {
    
    public:
    
      ...
    
      void sendClearMsg(const MsgInfo& info)
    
      {
    
        write "before sending" info to the log;
    
    
    
        sendClear(info);                          // if Company == CompanyZ,
    
                                                  // this function doesn't exist!
    
        write "after sending" info to the log;
    
      }
    
      ...
    
    
    
    };
    

    As the comment notes, this code makes no sense when the base class is MsgSender<CompanyZ>, because that class offers no sendClear function. That's why C++ rejects the call: it recognizes that base class templates may be specialized and that such specializations may not offer the same interface as the general template. As a result, it generally refuses to look in templatized base classes for inherited names. In some sense, when we cross from Object-oriented C++ to Template C++ (see Item 1), inheritance stops working.

    To restart it, we have to somehow disable C++'s "don't look in templatized base classes" behavior. There are three ways to do this. First, you can preface calls to base class functions with "this->":

    template<typename Company>
    
    class LoggingMsgSender: public MsgSender<Company> {
    
    public:
    
    
    
      ...
    
    
    
      void sendClearMsg(const MsgInfo& info)
    
      {
    
        write "before sending" info to the log;
    
    
    
        this->sendClear(info);                // okay, assumes that
    
                                              // sendClear will be inherited
    
        write "after sending" info to the log;
    
      }
    
    
    
      ...
    
    
    
    };
    
    

    Second, you can employ a using declaration, a solution that should strike you as familiar if you've read Item 33. That Item explains how using declarations bring hidden base class names into a derived class's scope. We can therefore write sendClearMsg like this:

    template<typename Company>
    
    class LoggingMsgSender: public MsgSender<Company> {
    
    public:
    
      using MsgSender<Company>::sendClear;   // tell compilers to assume
    
      ...                                    // that sendClear is in the
    
                                             // base class
    
      void sendClearMsg(const MsgInfo& info)
    
      {
    
        ...
    
        sendClear(info);                   // okay, assumes that
    
        ...                                // sendClear will be inherited
    
      }
    
    
    
      ...
    
    };
    
    

    (Although a using declaration will work both here and in Item 33, the problems being solved are different. Here, the situation isn't that base class names are hidden by derived class names, it's that compilers don't search base class scopes unless we tell them to.)

    A final way to get your code to compile is to explicitly specify that the function being called is in the base class:

    template<typename Company>
    
    class LoggingMsgSender: public MsgSender<Company> {
    
    public:
    
      ...
    
      void sendClearMsg(const MsgInfo& info)
    
      {
    
        ...
    
        MsgSender<Company>::sendClear(info);      // okay, assumes that
    
        ...                                       // sendClear will be
    
      }                                           //inherited
    
    
    
      ...
    
    };
    
    
    4.1
    In non-template code, replication is explicit: you can see that there's duplication between two functions or two classes. In template code, replication is implicit: there's only one copy of the template source code, so you have to train yourself to sense the replication that may take place when a template is instantiated multiple times.
    
    template<typename T,           // template for n x n matrices of
    
             std::size_t n>        // objects of type T; see below for info
    
    class SquareMatrix {           // on the size_t parameter
    
    public:
    
      ...
    
      void invert();              // invert the matrix in place
    
    };
    
    

    This template takes a type parameter, T, but it also takes a parameter of type size_t — a non-type parameter. Non-type parameters are less common than type parameters, but they're completely legal, and, as in this example, they can be quite natural.

    Now consider this code:

    SquareMatrix<double, 5> sm1;
    
    ...
    
    sm1.invert();                  // call SquareMatrix<double, 5>::invert
    
    
    
    SquareMatrix<double, 10> sm2;
    
    ...
    
    sm2.invert();                  // call SquareMatrix<double, 10>::invert
    
    

    Two copies of invert will be instantiated here. The functions won't be identical, because one will work on 5x5 matrices and one will work on 10 x 10 matrices, but other than the constants 5 and 10, the two functions will be the same. This is a classic way for template-induced code bloat to arise.


    template<typename T>
    
    class SquareMatrixBase {
    
    protected:
    
      SquareMatrixBase(std::size_t n, T *pMem)     // store matrix size and a
    
      : size(n), pData(pMem) {}                    // ptr to matrix values
    
    
    
      void setDataPtr(T *ptr) { pData = ptr; }     // reassign pData
    
      ...
    
    
    
    private:
    
      std::size_t size;                            // size of matrix
    
    
    
      T *pData;                                    // pointer to matrix values
    
    };
    
    template<typename T, std::size_t n>
    
    class SquareMatrix: private SquareMatrixBase<T> {
    
    public:
    
      SquareMatrix()                             // send matrix size and
    
      : SquareMatrixBase<T>(n, data) {}          // data ptr to base class
    
      ...
    
    
    
    private:
    
      T data[n*n];
    
    };
    
    
    Regardless of where the data is stored, the key result from a bloat point of view is that now many — maybe all — of SquareMatrix's member functions can be simple inline calls to base class versions that are shared with all other matrices holding the same type of data, regardless of their size. At the same time, SquareMatrix objects of different sizes are distinct types, so even though, e.g., SquareMatrix<double, 5> and SquareMatrix<double, 10> objects use the same member functions in SquareMatrixBase<double>, there's no chance of passing a SquareMatrix<double, 5> object to a function expecting a SquareMatrix<double, 10>


        
        
    • Templates generate multiple classes and multiple functions, so any template code not dependent on a template parameter causes bloat.

    • Bloat due to non-type template parameters can often be eliminated by replacing template parameters with function parameters or class data members.

    • Bloat due to type parameters can be reduced by sharing implementations for instantiation types with identical binary representations.(such as use void pointer instead of T* pointer)

    4.2 generalized copy constructors
    template<typename T>
    
    class SmartPtr {
    
    public:
    
      template<typename U>
    
      SmartPtr(const SmartPtr<U>& other)         // initialize this held ptr
    
      : heldPtr(other.get()) { ... }             // with other's held ptr
    
    
    
      T* get() const { return heldPtr; }
    
      ...
    
    
    
    private:                                     // built-in pointer held
    
      T *heldPtr;                                // by the SmartPtr
    
    };
    
    The generalized copy constructor above is not declared explicit. That's deliberate. Type conversions among built-in pointer types (e.g., from derived to base class pointers) are implicit and require no cast, so it's reasonable for smart pointers to emulate that behavior. Omitting explicit on the templatized constructor does just that.
    We use the member initialization list to initialize SmartPtr<T>'s data member of type T* with the pointer of type U* held by the SmartPtr<U>. This will compile only if there is an implicit conversion from a U* pointer to a T* pointer, and that's precisely what we want. The net effect is that SmartPtr<T> now has a generalized copy constructor that will compile only if passed a parameter of a compatible type.
    
    5. 
    implicit type conversion functions (if it is a class, it is ok)are never considered during template argument deduction. Never. Such conversions are used during function calls, yes, but before you can call a function, you have to know which functions exist. In order to know that, you have to deduce parameter types for the relevant function templates (so that you can instantiate the appropriate functions). But implicit type conversion via constructor calls is not considered during template argument deduction.
    a friend declaration in a template class can refer to a specific function
    
    template<typename T>
    
    class Rational {
    
    public:
    
      ...
    
    
    
    friend const Rational operator*(const Rational& lhs, const Rational& rhs)
    
    {
    
      return Rational(lhs.numerator() * rhs.numerator(),       // same impl
    
                      lhs.denominator() * rhs.denominator());  // as in
    
    }                                                          // Item 24
    
    };
    

    Now our mixed-mode calls to operator* will compile, because when the object oneHalf is declared to be of type Rational<int>, the class Rational<int> is instantiated, and as part of that process, the friend function operator* that takes Rational<int> parameters is automatically declared. As a declared function (not a function template), compilers can use implicit conversion functions (such as Rational's non-explicit constructor) when calling it, and that's how they make the mixed-mode call succeed.

    Inside a class template, the name of the template can be used as shorthand for the template and its parameters, so inside Rational<T>, we can just write Rational instead of Rational<T>
        
        
    • When writing a class template that offers functions related to the template that support implicit type conversions on all parameters, define those functions as friends inside the class template.

    6.
    
        
        

    Um, you don't remember your STL iterator categories? No problem, we'll do a mini-review. There are five categories of iterators, corresponding to the operations they support. Input iterators can move only forward, can move only one step at a time, can only read what they point to, and can read what they're pointing to only once. They're modeled on the read pointer into an input file; the C++ library's istream_iterators are representative of this category. Output iterators are analogous, but for output: they move only forward, move only one step at a time, can only write what they point to, and can write it only once. They're modeled on the write pointer into an output file; ostream_iterators epitomize this category. These are the two least powerful iterator categories. Because input and output iterators can move only forward and can read or write what they point to at most once, they are suitable only for one-pass algorithms.

    A more powerful iterator category consists of forward iterators. Such iterators can do everything input and output iterators can do, plus they can read or write what they point to more than once. This makes them viable for multi-pass algorithms. The STL offers no singly linked list, but some libraries offer one (usually calledslist), and iterators into such containers are forward iterators. Iterators into TR1's hashed containers (see Item 54) may also be in the forward category.

    Bidirectional iterators add to forward iterators the ability to move backward as well as forward. Iterators for the STL's list are in this category, as are iterators forsetmultisetmap, and multimap.

    The most powerful iterator category is that of random access iterators. These kinds of iterators add to bidirectional iterators the ability to perform "iterator arithmetic," i.e., to jump forward or backward an arbitrary distance in constant time. Such arithmetic is analogous to pointer arithmetic, which is not surprising, because random access iterators are modeled on built-in pointers, and built-in pointers can act as random access iterators. Iterators for vectordeque, andstring are random access iterators.

    Iterator Trail Implementation:

    template < ... >                    // template params elided
    
    class deque {
    
    public:
    
      class iterator {
    
      public:
    
        typedef random_access_iterator_tag iterator_category;
    
        ...
    
      }:
    
      ...
    
    };
    
     
    template<typename IterT>
    
    struct iterator_traits {
    
      typedef typename IterT::iterator_category iterator_category;
    
      ...
    
    };
    
    

    
    
    // partial template specialization

    template<typename IterT>       
    
    struct iterator_traits<IterT*>         // for built-in pointer types
    
    
    
    {
    
      typedef random_access_iterator_tag iterator_category;
    
      ...
    
    };
    

    template<typename IterT, typename DistT>              // use this impl for
    
    void doAdvance(IterT& iter, DistT d,                  // random access
    
                   std::random_access_iterator_tag)       // iterators
    
    
    
    {
    
      iter += d;
    
    }
    
    
    
    template<typename IterT, typename DistT>              // use this impl for
    
    void doAdvance(IterT& iter, DistT d,                  // bidirectional
    
                   std::bidirectional_iterator_tag)       // iterators
    
    {
    
      if (d >= 0) { while (d--) ++iter; }
    
      else { while (d++) --iter;         }
    
    }
    
    
    
    template<typename IterT, typename DistT>              // use this impl for
    
    void doAdvance(IterT& iter, DistT d,                  // input iterators
    
                   std::input_iterator_tag)
    
    {
    
      if (d < 0 ) {
    
         throw std::out_of_range("Negative distance");    // see below
    
      }
    
      while (d--) ++iter;
    
    }
    

    template<typename IterT, typename DistT>
    
    void advance(IterT& iter, DistT d)
    
    {
    
      doAdvance(                                              // call the version
    
        iter, d,                                              // of doAdvance
    
        typename                                              // that is
    
          std::iterator_traits<IterT>::iterator_category()    // appropriate for
    
      );                                                      // iter's iterator
    
    }                                                         // category
    

    7.

    That's what traits let you do: they allow you to get information about a type during compilation

    8.

    • Template metaprogramming can shift work from runtime to compile-time, thus enabling earlier error detection and higher runtime performance.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值