乐趣区

读书笔记Effective-C07模板与泛型

作者:LogM

本文原载于 https://segmentfault.com/u/logm/articles,不允许转载~

7. 模板与泛型

  • 7.1 条款 41:了解隐式接口和编译期多态

    • // 不用模板的写法
      class Widget {Widget();
          virtual ~Widget();
          virtual std::size_t size() const;
          virtual void normalize();
          void swap(Widget& other);
          ...
      };
      
      void doProcess(Widget& w) {if (w.size() > 10; && w!= ...) {Widget temp(w);
              temp.normalize();
              temp.swap(w);
          }
      }
      // w 支持的接口是类型 Widget 决定的,这称为 "显式接口"。//Widget 类里面的 virtual 函数是在运行期确定具体调用哪个函数,这称为 "运行期多态"。
    • // 使用模板的写法
      template<typename T>
      void doProcessing(T& w) {if (w.size() > 10 && w != ...) {T temp(w);
              temp.normalize();
              temp.swap(w);
          }
      }
      // w 支持的接口,是由 w 所参与执行的操作所决定的,比如例子中的 w 需要支持 size()、normalize()、swap()、拷贝构造、不等比较。这称为 "隐式接口"。// w 所参与执行的操作,都有可能导致 template 的具现化,使函数调用得以成功,具现化发生在编译期。这称为 "编译期多态"。
  • 7.2 条款 42:了解 typename 的双重意义

    • // 第一重意义
      template<class T> class Widget;
      template<typename T> class Widget;
      // 上面两句话效果完全一样 
    • // 第二重意义
      // 考虑一个例子
      template<typename C>
      void print2nd(const C& container) {
          C::const_iterator* x;   //bad,不加 typename 被假设为非类型,理由见下面注释
          ...
      }
      // 一般,我们认为 C::const_iterator 指的是某种类型,但是存在一种逗比情况:// C 是一个类,const_iterator 是这个类的 int 型的成员变量,x 是一个 int 型的变量,那么上面一句话就变成了两个 int 的相乘。// 正因为有这种歧义情况的存在,C++ 假设不加 typename 的 "嵌套从属名称" 是非类型。// 应该这么写
      template<typename C>
      void print2nd(const C& container) {
          typename C::const_iterator* x;   //ok,告诉编译器,C::const_iterator 是类型
          ...
      }
  • 7.3 条款 43:学习处理模板化基类内的名称

    • // 基类
      template<typename T>
      class MsgSender {
      public:
          ...
          void sendClear(const MsgInfo& info);
          ...
      };
      // 派生类
      template<typename T>
      class LoggingMsgSender : public MsgSender<T> {
      public:
          void sendClearMsg(const MsgInfo& info) {sendClear(info);   //bad,理由见下方注释
          }
      }
      // 编译器遇到 LoggingMsgSender 类时,不知道要继承哪种 MsgSender 类,所以编译器不知道 sendClear 这个函数是 MsgSender 类里继承下来的成员方法,还是类外面的全局的函数。// 为什么说不同的 MsgSender 类不一定有 sendClear 成员方法呢?因为 C ++ 允许 template 的特化,比如我在下面写了一个特化的类,这个特化的类为空类,就没有 sendClear 成员方法。template<>
      class MsgSender<CompanyZ> { };
      
      // 解决这个问题的方法,本质就是告诉编译器,sendClear 函数的来源。具体来说,有三种方法:// 方法 1
      template<typename T>
      class LoggingMsgSender : public MsgSender<T> {
      public:
          void sendClearMsg(const MsgInfo& info) {this->sendClear(info);   //ok,告诉编译器,sendClear 函数是类内的成员方法
          }
      }
      // 方法 2
      template<typename T>
      class LoggingMsgSender : public MsgSender<T> {
      using MsgSender<T>::sendClear; // 先声明,告诉编译器,如果遇到 sendClear 函数,则视为类内的成员方法进行编译
      public:
          void sendClearMsg(const MsgInfo& info) {sendClear(info);   //ok
          }
      }
      // 方法 3
      template<typename T>
      class LoggingMsgSender : public MsgSender<T> {
      public:
          void sendClearMsg(const MsgInfo& info) {MsgSender<T>::sendClear(info);   //ok,告诉编译器,sendClear 函数是类 MsgSender<T> 内的成员方法
          }
      }
      // 方法 3 不太好的地方是,假如 sendClear() 是 virtual 函数,这种写法会把它的多态性破坏;方法 1 和方法 2 则不会破坏。
  • 7.4 条款 44:将与参数无关的代码抽离 templates

    • 编译器对 template 的处理,实际上是对所有可能的 template 具现出具体代码
    • // 模板类
      template<typename T, std::size_t n>
      class SquareMatrix {
      public:
          ...
          void invert();    // 该函数与 template 无关}
      // 使用
      SquareMatrix<double, 5> sm1;
      SquareMatrix<double, 10> sm2;
      sm1.invert();
      sm2.invert();
      // 这个例子中,invert() 函数与 template 无关,但它被编译器生成了两份,造成重复。
    • 作者认为将与参数无关的代码抽离 templates,可以避免编译器产生这类的重复代码;但我觉得有时候要达到这个目的,会造成代码可读性和编写效率的下降,实际使用时还是要权衡。
  • 7.5 条款 45:运用成员函数模板接受所有兼容类型

    • 假设派生类 D 继承于基类 B,由 B 具现化的模板类和由 D 具现化的模板类,并不能相互转换。以代码表述:
    • class B {...};
      class D : public B {...};
      
      template<typename T>
      class SmartPtr {
      public:
          SmartPtr(T* realPtr);
          ...
      }
      
      // 使用
      SmartPtr<B> pt1 = SmartPtr<D>(new D);  //bad,SmartPtr<B> 与 SmartPtr<D> 没有继承关系来使得他们相互转换
      
      // 解决方法
      template<typename T>
      class SmartPtr {
      public:
          SmartPtr(T* realPtr);
          template<typename U> SmartPtr(const SmartPtr<U>& other);  // 建立一个泛化拷贝构造函数,来解决上面的问题
          ...
      }
      // 当然,对于赋值函数也可以这么操作 
  • 7.6 条款 46:需要类型转换时,请为模板定义非成员函数

    • 这条把条款 24 扩充到模板类上。
    • template<typename T>
      class Rational {
      public:
          ...
          Rational(const T& numerator, const T& denominator);
          Rational(sonst T& num);  
          const T numerator() const;
          const T denominator() const;}
      
      const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs) {...}
      // 使用
      Rational<int> lhs(1, 9);
      Rational<int> result;
      result = lhs * 2;   //bad,template 的推导不考虑隐式类型转换,编译器猜不出 T 是什么
      result = 2 * lhs;   //bad,template 的推导不考虑隐式类型转换,编译器猜不出 T 是什么
      
      // 解决方法
      template<typename T>
      class Rational {
      public:
          ...
          Rational(const T& numerator, const T& denominator);
          Rational(sonst T& num);  
          const T numerator() const;
          const T denominator() const;
      
          friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
              // 这里要把类外面 operator* 实现的代码拷贝一份到这里
              ...
          }  
          // 在类内声明 friend 函数,使编译器在类初始化时可以先具现出:
          //"const Rational<int> operator* (const Rational<int>& lhs, const Rational<int>& rhs)"
      };
      const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs) {...}
      // 使用
      Rational<int> lhs(1, 9);
      Rational<int> result;
      result = lhs * 2;   //ok,由于 friend 函数带来的具现化,编译器执行到这里时,具现化好的函数中,已经有满足需要的了,不需要推导 T
      result = 2 * lhs;   //ok,由于 friend 函数带来的具现化,编译器执行到这里时,具现化好的函数中,已经有满足需要的了,不需要推导 T 
  • 7.7 条款 47:使用 traits classes 表现类型信息

    • STL 中广泛使用 traits classes 来标记容器属于哪一类容器(比如 ” 可随机访问容器 ”:vector、deque 等)
  • 7.8 条款 48:认识 template 元编程(TMP)

    • 所谓元编程,是执行于编译器内的程序,C++ 以 template 实现元编程。
    • 优点:a. 完成一些以前不可能完成的任务;b. 将工作从运行期转移到编译期(比如之前在运行期才找到的错误可以在编译期找到)。
    • 缺点:编译时间变长。
    • TMP 不同于 ” 正常化的 ”C++,还没有完全被 C ++ 标准支持,普通用户可以暂时不用了解。
退出移动版