zoukankan      html  css  js  c++  java
  • C++ Primer学习笔记

    模板与泛型编程

    OOP,能处理类型在程序运行之前都未知的情况;泛型编程,在编译时能获取类型。
    模板是泛型编程的基础。本章学习如何定义自己的模板。

    16.1 定义模板

    问题引出:假设希望编写一个函数来比较2个值,并指出第一个值是<, > or == 第二个值。实际编程中,可能想要定义多个重载函数,每个函数比较一种给定类型的值。这样就会写很多函数体一样的函数,而仅仅是函数类型不同,很繁琐。我们使用函数模板解决这个问题。

    // 下面2个函数用于比较v1和v2的大小,仅仅是函数参数类型不一样,函数体完全一样
    // string版本
    int compare(const string &v1, const string &v2) {
      if (v1 < v2) return -1;
      else if(v1 > v2) return 1;
      return 0;
    }
    // double版本
    int compare(const double&v1, const double&v2) {
      if (v1 < v2) return -1;
      else if(v1 > v2) return 1;
      return 0;
    }
    

    16.1.1 函数模板

    以形如template 开始,为函数定义类型参数,可以实例化出特定函数的模板,就是函数模板。
    类型参数T,可以看作类型说明符,作为函数返回值类型或者形参类型。会在调用函数时,编译器利用实参类型推断出T代表的类型。

    什么叫(函数模板)实例化?
    当调用一个函数时,编译器用函数实参推断出的模板参数,用此实际实参代替模板参数来创建出一个新的“实例”,也就是一个真正可以调用的函数,这个过程叫实例化。
    编译器生成的函数版本,通常称为模板的实例。

    // 定义compare的函数模板
    // compare声明了类型为T的类型参数
    template <typename T> // template关键字, <typename T> 是模板参数列表,typename和class关键字等价,都可以使用,T是模板参数,以逗号分隔其他模板参数
    int compare(const T &v1, const T &v2) {
      if (v1 < v2) return -1;
      else if (v1 > v2) return 1;
      return 0;
    }
    
    // 调用模板,将模板实参绑定到模板参数(T)上
    // 调用函数模板时,编译器根据函数实参(1,0)来推断模板实参
    cout << compare(1, 0) << endl;  // T为int
    
    // 编译器会根据调用情况,推断出T为int,从而生成一个compare版本,T被替换为int
    int compare(const int &v1, const int &v2) {
      if (v1 < v2) return -1;
      else if (v1 > v2) return 1;
      return 0;
    }
    

    模板类型参数
    类型参数可以看做类型说明符,像内置类型或类类型说明符一样使用,单仅限于定义模板的函数返回类型、参数类型、函数体内变量声明、类型转换。
    类型参数前必须使用关键字typename或class,两者等价,可互换。(仅限于模板参数列表中)

    template <typename T> T foo(T *p) {
      T tmp = *p; // tmp类型T,是指针p指向的类型
      // ...
      return tmp;
    }
    
    // 错误使用示例
    template <typename T, U> T calc(const T&, const U&); // U 之前必须加typename或class
    // 正确
    template <typename T, class U> T calc(const T&, const U&); 
    

    非类型模板参数
    非类型参数表示一个值,而非一个类型。通过一个特定类型名而非(typename/class)来指定非类型参数。
    当一个模板实例化时,非类型参数被用户提供的,或编译器推断出的值所代替。这些值必须是常量表达式。

    注意:

    • 绑定到非类型整型参数的实参必须是一个常量表达式;
    • 绑定到指针或引用非类型参数的实参必须具有静态生存期;
    template <unsinged N, unsigned M> // N, M是非类型整型参数
    int compare(const char &(p1)[N], const char &(p2)[M]) {
      return strcmp(p1, p2);
    }
    
    // 调用compare时,编译器会用字面量大小来替代非类型参数N和M
    compare("hi", "mom"); // N = 3, M = 4,注意编译器会自动在字符串末尾添加""作为终结符
    

    inline和constexpr的函数模板
    声明inline或constexpr的函数模板,inline/constexpr说明符要放在模板参数列表之后,返回类型之前:

    // 正确,inline放在template模板参数之后,返回值类型之前
    template <typename T> inline T min(const T&, const T&);
    // 错误,inline放到了template之前
    inline template <typename T> T min(const T&, const T&);
    

    编写类型无关的代码*
    前面compare函数,说明了编写泛型代码的2个重要原则:

    • 模板中的函数是const引用
    • 函数体中条件判断仅使用< 比较运算

    但是,编写代码如果使用了 <, >运算符,就降低了compare对要处理类型的要求。也就是说,这些类型必须要支持<,>。
    如果真的关心类型无关和可移植性,可能需要用到less(标准库函数,头文件 algorithm)来定义compare函数。

    // 实际上less函数也用到了<,并没有起到更良好定义的作用
    template <typename T> int compare(const T &v1, const T&v2) {
      if (less<T>()(v1, v2)) return -1; // <=> if(v1 < v2)
      else if(less<T>()(v2, v1)) return 1;
      return 0;
    }
    

    模板编译*
    编译器在模板定义时,不生成代码。只有实例化出模板的一个特定版本时,编译器才会生成代码。

    函数模板和类模板成员函数的定义通常放在头文件中。

    16.1.2 类模板

    可以实例化出特定类的模板,叫类模板。
    类模板是用来生成类的蓝图的。与函数模板的区别是,编译器不能为类模板推断模板参数类型。

    template <typename T> class Blob { // 类型为T的模板类型参数
    public:
        typedef T value_type;
        typedef typename std::vector<T>::size_type size_type;
        // 构造函数
        Blob();
        Blob(std::initializer_list<T> il);
    
        // Blob中的元素数目
        size_type size() const { return data->size(); }
        bool empty() const { return data->empty(); }
        // 添加和删除元素
        void push_back(const T &t) { data->push_back(t); }
    
        // 移动版本
        void push_back(T &&t) { data->push_back(std::move(t));}
        void pop_back();
    
        // 元素访问
        T& back();
        T& operator[](size_type i);
        
    private:
        std::shared_ptr<std::vector<T>> data;
        // 若data[i]无效,则抛出msg异常信息
        void check(size_type i, const std::string &msg) const;
    };
    

    实例化类模板
    要使用类模板,必须提供额外信息,即显示模板实参列表,绑定到模板参数。编译器可以用这些模板实参实例化出特定的类。

    一个类模板的每个实例都是一个独立的类,比如Blob与Blob没有任何关联,也不会对任何其他Blob类型的成员有特殊访问权限。

    // 使用特定类型版本的Blob(即Blob<int>),必须提供元素类型
    Blob<int> ia; // 构建空Blob<int>
    Blob<int> ia2 = {0,1,2,3,4}; // 构建包含5个元素的Blob<int>
    
    // 使用Blob<string> 版本
    Blob<string> names;
    // 使用Blob<double> 版本
    Blob<double> prices;
    

    编译器实例化出一个与下面定义等价的类:

    // 注意:所有模板参数T都被编译器根据显式模板实参,替换为对应的类型
    template<> class Blob<int> {
      typedef typename std::vector<int>::size_type size_type;
      Blob();
      Blob(std::initializer_list<int> il);
      // ...
      int& operator[](size_type i);
    
    private:
      std::shared_ptr<std::vector<int>> data;
      void check(size_type i, const std::string &msg) const;
    }
    

    在模板作用域中引用模板类型
    类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
    也就是说,template class Blob{...} 这里模板名称Blob不是一个类型名,而模板参数T当做被使用模板的实参。

    简而言之,就是类模板参数T,可以在类内部成员定义时使用,而T所代表的类型取决于实例化Blob传入的类型(xxx)。

    // data定义,使用了Blob的类型参数T,来声明data是一个share_ptr的实例
    std::shared_ptr<std::vector<T>> data;
    
    // 实例化特定类型Blob<string>后,data成为
    shared_ptr<vector<string>> data;
    

    类模板的成员函数
    类模板的成员函数是一个普遍函数,每个实例化的类,都有自己版本的成员函数。
    如check, back, operator[]

    template<typename T>
    void Blob<T>::check(Blob::size_type i, const std::string &msg) const { // 检查当前位置i是否合法
        if (i >= data->size()) throw std::out_of_range(msg);
    }
    
    template<typename T>
    T &Blob<T>::back() {
        check(0, "back on empty Blob");
        return data->back();
    }
    
    template<typename T>
    T &Blob<T>::operator[](Blob::size_type i) {
        // 如果i太大,check抛出异常,阻止访问不存在的元素
        check(i, "subscripte out of range");
        // return data[i]; // 错误,data是一个指向vector<T>的shared_ptr,vector下标访问需要先解引用
        return (*data)[i];
    }
    
    template<typename T>
    void Blob<T>::pop_back() { // 弹出末尾元素
        // 检查data指向的vector是否为空
        check(0, "pop_back on empty Blob");
        data->pop_back();
    }
    
    

    构造函数

    template<typename T>
    Blob<T>::Blob() : data(std::make_shared<std::vector<T>>()){ // 构造函数
    }
    
    template<typename T>
    Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>>(il)) { // 初始化列表构造函数
    }
    
    // 使用了上面的构造函数,Blob对象就能像下面这样构造
    Blob<string> articles = {"a", "an", "the"};
    

    类模板成员函数的实例化
    默认情况下,类模板成员函数只有当程序用到它时才实例化。

    类内、类外使用模板类名*
    类的作用域内,可以直接使用模板名而不必指定模板实参.。

    // 注意模板名后面的类型参数列表<T>
    
    // 类内可以使用简化名称
    Blob &Blob(Blob &&); // 移动构造函数
    Blob &operator++(); // 前置自增 <=> Blob<T> &operator()
    
    // 类外定义成员时,不在类的作用域,要指出类型参数T
    template <typename T>
    Blob<T> Blob<T>::operator++(int);  // 后置自增
    

    类模板和友元
    当一个模板类包含一个友元声明时,类与友元各自是否模板无关?
    如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。 如果友元自身是模板,类可以授权所有友元模板实例,也可以只授权给特定实例。

    一对一友好关系
    引用(类或资源)模板的一个特定实例
    步骤:

    1. 声明模板自身;
    2. 在类内声明友元关系;
    // 注意1对1友元关系中,友元声明和类模板本身不同之处
    
    // 前置声明,在Blob中声明友元所需要
    template <typename> class BlobPtr; // 声明友元函数需要
    
    template <typename> class Blob; // 运算符== 参数列表需要
    template <typename T> bool operator==(const Blob<T> &, const Blob<T> &); // 声明要作为友元的函数
    
    template <typename T> class Blob {
      friend class BlobPtr<T>; // 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
      freind bool operator==<T>(const Blob<T> &, const Blob<T> &);
      //其他成员定义
      ...
    };
    

    通用和特定的模板友好关系
    一个类将另一个类声明为友元,情况分为两大类:
    1.非模板类中,声明友元类:声明的类可以是模板类,也可以是非模板类(普通友元声明);
    2.模板类中,声明友元类:声明的类可以模板类,也可以是非模板类;

    // 前置声明,在C和C2中声明友元所需
    template <typename T> class Pal; 
    // 注意这里没有Pal2的前置声明
    
    class C{ // C是一个普遍非模板类
      friend class Pal<C>; // (用类C)实例化的Pal是C的一个友元,1对1友元关系。
    
      template <typename T> friend class Pal2; // Pal2所有实例都是C的友元, 因为已经包含了模板参数列表, 不需前置声明
    };
    
    template <tyepname T>  class C2 { // C2是一个模板类
      
      friend class Pal<T>;  // C2的每个实例,将相同实例化的Pal声明为友元
    
      template <typename X> friend class Pal2; // Pal2的所有实例都是C2的友元,不需要前置声明。这里X代表Pal2使用的模板参数,跟C2使用的T不一样
    
      friend class Pal3; // Pal3是非模板类,是C2所有实例的友元。不需要前置声明
    };
    
    

    令模板自己的类型参数成为友元
    模板类可以将自己的类型参数,声明为友元

    template <typename T> class Bar {
      friend T; // 将类的访问权限,授予用来实例化的Bar类型 (模板类实例化后的类)
      // ...
    };
    

    模板类型别名
    用typedef定义引用实例化的类的别名,用using定义引用模板类的别名。

    typedef Blob<string> StrBlob;  // 正确,引用的是Blob<string>,属于模板的一个实例,StrBlob是Blob<string>的别名
    
    typedef Blob<T> StrBlob;       // 错误,由于Blob<T>模板不是一个类型,不能用typedef引用一个模板类
    
    template <typename T> using StrBlob = Blob<T>; // 正确,StrBlob是模板类的别名
    StrBlob<int> b1;    // <=> Blob<int>
    StrBlob<double> b2; // <=> Blob<double> 
    StrBlob<string> b3; // <=> Blob<string>
    

    类模板的static成员
    所有实例化的类,都包含自己的static成员。

    如下面的类模板,一个给定的实例化的类Foo,包含一个共同的static 成员。不同的实例化的类,包含不同的static成员。

    template <typename T> class Foo {
    public:
      static std::size_t count() { return ctr; }   // static函数成员
      // ...
    private:
      static std::size_t ctr; // static数据成员
      // ...
    };
    

    16.1.3 模板参数

    类似函数参数的名字,模板参数的名字只是一个符号,没有什么含义,T只是习惯上的命名。

    模板参数与作用域
    模板参数的作用域从声明之后,到模板声明/定义结束之前。而且,模板内不能重用模板参数名。

    typedef double A;
    template <typename A, typename B> void f(A a, B b) // 模板参数A,B的作用域从声明之后,到模板声明/定义结束之前
    { 
      A tmp = a; // 覆盖了typedef对A的定义,A代表的类型由函数模板实例化决定
      double B;  // 错误:模板参数名不能重用
    };
    

    模板声明
    声明,但不定义模板,不过必须包含模板参数。声明和定义中的参数名称,不必相同。

    // 声明,但不定义模板
    template <typename T> int compare(const T&, const T&);
    template <typename T> class Blob;
    
    // 3个声明/定义都指向相同的函数模板
    template <typename T> T calc(const T&, const T&); // 声明
    template <typename U> U calc(const U&, const U&); // 声明
    
    template <typename X> X calc(const X& a, const X& b) { // 定义
    // ...
    }
    

    使用类的类型成员
    如何通过类模板参数T,使用实例化之后T内定义的类型?
    如果直接用作用域运算符(::)这样做,编译器无法判断是想使用T的静态成员value_type,还是想使用T内类型valuetype。

    解决办法:通过typename显示告诉编译器,该名字是一个类型,而非static成员。
    通知编译器一个名字表示类型时,只能用typename, 不能用class

    // 声明类型的错误方式
    T::value_type
    
    // 声明类型的正确方式
    template <typename T> 
    typename T::value_type top(const T& c) { // 注意这里的typename T::value表明这是一个类型
      if(!c.empty()) return c.back();
      else return typename T::value_type(); // 疑问:这里如果T::value_type表示类型,为何会带一个() ? 答案是这里的 类型+(),会调用默认构造函数(对类)或者内置的初始化方法(对内置类型,如int,初值一般为0)
    };
    

    默认模板实参
    可以像指定函数默认实参一样,为模板参数提供默认实参。

    template <typename T, typename F = less<T>> // F默认值是less<T>,一个模板类,重载了函数调用运算符(operator())
            int compare(const T &v1, const T &v2, F f = F()) { // 这里F(),是相当于调用less<T>(),也就是less<T>的函数调用重载版本
        if (f(v1, v2)) {
            return -1;
        }
        if (f(v2, v1)) {
            return 1;
        }
        return 0;
    }
    
    // 调用函数模板实例
    auto i = compare(0, 42); // i = -1
    

    注意:类模板同样也可以为类型参数,指定默认实参,也可以省略指定默认实参的部分。

    16.1.4 成员模板

  • 相关阅读:
    Head first javascript(七)
    Python Fundamental for Django
    Head first javascript(六)
    Head first javascript(五)
    Head first javascript(四)
    Head first javascript(三)
    Head first javascript(二)
    Head first javascript(一)
    Sicily 1090. Highways 解题报告
    Python GUI programming(tkinter)
  • 原文地址:https://www.cnblogs.com/fortunely/p/14564383.html
Copyright © 2011-2022 走看看