C/C++教程

C++ Primer学习笔记 - 第16章 模板与泛型编程

本文主要是介绍C++ Primer学习笔记 - 第16章 模板与泛型编程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 模板与泛型编程
    • 16.1 定义模板
      • 16.1.1 函数模板
      • 16.1.2 类模板
      • 16.1.3 模板参数
      • 16.1.4 成员模板

模板与泛型编程

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,注意编译器会自动在字符串末尾添加"\0"作为终结符

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

这篇关于C++ Primer学习笔记 - 第16章 模板与泛型编程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!