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 成员模板

  • 相关阅读:
    java-version
    Centos7使用google-Chrome浏览器
    sign_and_send_pubkey: signing failed: agent refused operation
    删除带中划线的数据库
    yum update报错RPM数据库问题
    Centos的timedatectl
    /var/lib/docker/overlay2/db72dadf047395c178feb0eaedd932b48e283abcaa5a870c0c746bab8ee6f76a: no such file or directory
    /bin/bash^M: 坏的解释器: 没有那个文件或目录
    repeat()函数
    (Problem 47)Distinct primes factors
  • 原文地址:https://www.cnblogs.com/fortunely/p/14564383.html
Copyright © 2011-2022 走看看