简介

  • C++ 模板相关笔记

C++ 模板是什么

C++ 模板是一种编程工具,允许程序员编写通用代码,不受特定数据类型的限制。它们允许在编译时进行参数化类型,允许创建函数模板和类模板。

模板在 C++ 中用于生成通用代码,这些代码可以根据不同的数据类型或参数进行实例化。主要有两种类型的模板:

  1. 函数模板(Function Templates):允许定义一个通用函数,可以在不同数据类型下工作。例如,可以编写一个模板函数来交换两个值,无论这两个值的类型是什么。
1
2
3
4
5
6
template <typename T>
void swapValues(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}
  1. 类模板(Class Templates):允许定义一个通用类,可以在不同数据类型下使用。例如,可以创建一个模板类来实现动态数组,其中数组的元素类型可以根据需要指定。
1
2
3
4
5
6
7
8
9
10
11
template <typename T>
class DynamicArray {
private:
    T *array;
    int size;
public:
    DynamicArray(int s) : size(s) {
        array = new T[size];
    }
    // 其他成员函数和操作...
};

模板使用关键字 template 来声明,并且通常使用 typenameclass 关键字定义模板参数。在使用模板时,编译器会根据具体的使用情况生成特定类型的代码。例如,可以使用 swapValues 函数模板交换整数、浮点数或自定义类型等。

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
    int num1 = 5, num2 = 10;
    swapValues(num1, num2); // 调用模板函数交换整数

    double val1 = 3.14, val2 = 6.28;
    swapValues(val1, val2); // 调用模板函数交换浮点数

    // 使用类模板
    DynamicArray<int> intArray(10); // 创建一个存储整数的动态数组
    DynamicArray<double> doubleArray(20); // 创建一个存储双精度浮点数的动态数组

    return 0;
}

模板是 C++ 中非常强大和灵活的功能,能够提高代码的复用性,并允许以一种通用的方式处理不同类型的数据。

模板

  • 模板,是一种用(其他)类型和算法对类型和算法进行参数化的机制。用户自定义类型和内置类型的计算表现为函数,有时泛化为模板函数和函数对象。

  • template <typename T> : 指明T是该声明的形参,它是数学上“对所有T”或“对所有类型T”的C++表达。

  • 从一个模板和一个模板实参列表生成一个类或者一个函数的过程通常被称为模板实例化(template instantiation)。一个模板针对某个特定模板实参列表的版本被称为特例化(specialization)
  • 一般来说,保证从所有的模板实参列表生成模板的特例化是C++实现的责任,而不是程序员的任务。

  • 模板是什么?或者换句话说,当使用模板时,什么程序设计技术更有效?模板提供了:
    • 传递类型参数(像传递值和模板一样)而不丢失信息的能力。则意味着有大好的机会进行内联,当前的C++实现的确充分利用了这一点
    • 推迟的类型检查(在实例化时进行)。这意味着有机会将来自不同上下文的信息编织在一起
    • 传递常量参数的能力。这意味着能进行编译时计算
  • 换句话说,模板为编译时计算和类型处理提供了一种强有力的机制,可以生成非常紧凑和高效的代码。记住:类型(类)既可以包含代码也可以包含值。

  • 模板的首要用途,也是它最常见的用途,是支持泛型程序设计(generic programming),即关注通用算法设计,实现和使用的程序设计。
  • 在这里,”通用“的含义是算法可以接受各种各样的实参类型,只要这些类型满足算法对实参的要求即可。模板是C++支持泛型程序设计的主要特性。它提供了(编译时)参数化多态。
  • 更多地关注代码生成技术(将模板看作类型和函数的生成器),并依赖类型函数表示编译时计算的编程方式被称为模板元程序设计(template metaprogramming)

C++模板详解

  • 模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数,返回值取得任意类型

  • 模板是一种对类型进行参数化的工具;
  • 通常有两种形式:函数模板和类模板
    • 函数模板针对仅参数类型不同的函数
    • 类模板针对仅数据成员和成员函数类型不同的类
  • 使用模板的目的就是能够让程序员编写与类型无关的代码
  • 模板的声明或定义只能在全局,命名空间或类范围内进行。即,不能在局部范围,函数内进行声明。例如:不能在main()函数中声明或定义一个模板。

  • 函数模板通式
    • 函数模板的格式:
      • template <class 形参名,class 形参名,......>返回类型 函数名(参数列表){函数体}
    • 其中,templateclass是关键字,class可以用typename关键字代替,在这里typenameclass没有什么区别
    • <>括号中的参数叫模板形参,模板形参和函数形参很像,模板形参不能为空。
    • 一旦声明了模板函数就可以用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内只类型的地方都可以使用模板形参名
    • 模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型,就称它实例化了函数模板的一个实例。
    • 当调用这样的模板函数时,类型T就会被调用时的类型所代替。例如:swap(a,b)其中a和b是int型,这时模板函数swap中的形参T就会被int所代替,模板函数就变为swap(int &a, int &b)。而当swap(c,d)其中c和d是double类型时,模板函数会被替换为swap(double&a, double &b)。如此以来,就实现了函数的实现与类型无关的代码。
    • 注意
      • 对于函数模板而言不存在h(int, int)这样的调用,不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参推演来进行,即只能进行h(2,3)这样的调用,或者int a,b; h(a,b)
  • 类模板通式
    • 类模板的格式为:template<class 形参名,class 形参名,...> class 类名 {...};
    • 类模板和函数模板都是以template开始后接模板形参列表组成,模板形参不能为空,一旦声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来表示。例如template<class T> class A{public: T a; T b; T hy(T c, T &d)};
    • 在类A中声明了两个类型为T的成员变量ab,还声明了一个返回类型为T带两个参数类型为T的函数hy
    • 类模板对象的创建:
      • 例如一个模板类A,则使用类模板创建对象的方法为A<int> m;在类A后面跟上一个<>尖括号并在里面天上相应的类型,这样的话类A中凡是用到模板形参的地方都会被int所代替。当类模板有两个模板形参时创建对象的方法为A<int, double> m;类型之间使用逗号隔开。
      • 对于类模板,模板形参的类型必须在类名后的尖括号中明确指定。例如:A<2> m;使用这种方法把模板形参设置为int是错误的(编译错误:error C2079:'a' uses undefined class 'A<int>'),类模板形参不存在实参推演的问题。也就是说:**不能把整型值2推演为int型传递给模板形参,要把类模板形参调置为int型必须这样指定`A m**。
    • 再次注意:模板的声明或定义只能在全局,命名空间或类范围内进行,即不能在局部范围,函数内进行。

模板的形参

  • 有三种类型的模板形参:类型形参,非类型形参和模板形参

类型形参

  • 类型模板形参:类型形参由关键字classtypename后接说明符构成,例如template<class T> void h(T, a){};其中,T就是一个类型形参,类型形参的名字由用户自己确定
  • 模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等
  • 函数模板:不能为同一个模板类型形参指定两种不同的类型,例如template<class T>void h(T a, Tb){}, 语句调用h(2, 3.2)将出错
    • 因为该语句给同一个模板形参T指定了两种类型,第一个实参2把模板形参T指定为int,而第二个实参3.2把模板形参指定为double,两种类型的形参不一致,会出错
  • 类模板:当我们声明类对象为:A<int> a,例如:template<class T>T g(T a, T b){},语句调用a.g(2, 3.2)在编译时不会出错,但是会有警告。
    • 因为在声明类对象的时候已经将T转换为T类型,而第二个实参3.2把模板形参指定为double,在运行时,会对3.2进行强制类型转换为3。
    • 当我们声明类的对象为:A<double> a,此时,就不会有上面的警告,因为从int到double时自动类型转换。

非类型形参

  • 非类型模板形参:模板的非类型形参也就是内置类型形参,例如:template <class T, int a> class B{};其中int a就是非类型的模板形参
  • 非类型形参在模板定义的内部时常量值,也就是说非类型形参在模板的内部是常量
  • 非类型模板的形参只能是整形,指针和引用
  • 调用非类型模板形参的实参必须是一个常量表达式,即它必须能在编译时计算出结果
  • 注意:
    • 任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。
    • 全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参
  • 全局变量的地址或引用,全局对象的地址或引用const类型常量是常量表达式,可以用作非类型模板形参的实参
  • 非类型形参一般不应用于函数模板中

类模板的默认模板类型形参

  • 可以为类模板的类型形参提供默认值,但是不能为函数模板的类型形参提供默认值。
  • 函数模板和类模板都可以为模板的非类型形参提供默认值
  • 类模板的类型形参默认值形式为:template <class T1, class T2=int> class A{};为第二个模板类型形参T2提供int型的默认值
  • 类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,例如:template<class T1=int, class T2> classA{};就是错误的,因为T1给出了默认值,而T2没有设定
  • 在类模板的外部定义类中的成员时,template后的形参表应该省略默认的形参类型。例如:template<class T1, class T2=int> class A{public:void h();};定义方法为:template<class T1, clas T2> void A<T1, T2>::h(){}

算法和提升

  • 函数模板就是普通函数的泛化:它能对多种数据类型执行动作,并且能够用以参数方式传递来的各种操作实现要执行的动作。
  • 算法(algorithm)就是一个求解问题的过程或公式:通过一个有穷的计算序列生成结果。因此,函数模板通常也称为算法。

  • 如何将一个在特定数据类型上执行特定操作的函数泛化为一个在多种数据类型上执行更通用操作的算法呢?最有效的方法是从一个(多个可能更好)具体实例来泛化出一个好的算法。这种泛化过程就称为提升(lifting):即,从特殊函数提升为一个通用算法。
  • 在这样一个由具体到抽象的过程中,最重要的一点是保持性能并注意如何做才合理。

概念

  • 有效处理模板参数传递问题的第一步是建立一个用于讨论模板实参的要求的框架和词汇表。
  • 我们可以将一组对模板实参的要求看作一个谓词。例如,可以将“C必须是一个容器”看作一个谓词,它接受一个类型参数C,若C是一个容器则返回true(我们应该已经定义了什么是“容器”),否则返回false。
  • 例如,Container<vector<int>>()Container<list<string>>()应该为真,而Container<int>()Container<shared_ptr<string>>()应该为假。
  • 我们称这种谓词概念(concept)。概念并非C++中的语言结构,它是一种理念,可以用来推理对模板实参的要求,可以用于注释中,有时可以用我们自己的代码来实现。
  • 初学者可以将一个概念看作一个设计工具:通过一组注释来说明Container<T>(),指出T必须满足什么性质才能使Container<T>()为真。例如
    • T必须有下标运算符([])
    • T必须有成员函数size()
    • T必须有成员类型value_type,它是元素的类型。
  • 就像“普通类”一样,类模板可以有任意类型的数据成员。非static数据成员可以在其定义时初始化,也可以在构造函数中初始化
  • 与”普通类“一样,非static成员函数的定义可以在类模板内部,也可以在外部

概念和约束

  • 概念不是任意的属性集合。大多数类型(或一组类型)的属性列表并不能给出一个一致,有用的概念定义。要成为一个有用的概念,要求列表必须反映模板类的一组算法或一组操作的需求。

  • 我为概念设定的标准非常高:我要求一个概念具有通用性,一定程度的稳定性,广泛的算法适用性,语义一致性以及其他很多性质。实际上,按照我的标准,很多常见的模板实参的简单约束都不够格称为概念。
  • 我认为这是不可避免的。特别是,我们编写过很多模板,它们并不能很好地反映通用算法或广泛应用的类型。相反,它们的重点是实现细节,它们的实参只反映了单一模板的必要细节,而这些模板只是为特定实现中的特定用途而设计的。我将这种模板实参的要求称为约束(constraint)或特殊概念(ad hoc concept)