初探模板(函数模板,类模板)


前言

本章参考了C++ 模板常见特性(函数模板、类模板),和《C++ Primer Plus 6th中文版》

  • 函数模板
  • 类模板

使用「模板」的特性设计,实际上也就是「泛型」程序设计。


一、函数模板

函数模板是一种工具,允许程序员编写与数据类型无关的代码。函数模板可以用来创建可用于不同数据类型的函数,而无需为每种类型编写单独的函数。这种方法提高了代码的可重用性和灵活性。

函数模板通过使用泛型来定义函数的功能,让你可以用相同的函数体处理不同的数据类型。在编译时,编译器根据函数模板生成具体类型的函数定义。这个过程称为模板实例化。

typename关键字使得参数AnyType表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的。在这种上下文中,这两个关键字是等价的

基本语法

函数模板的声明以关键字 template 开始,后跟模板参数列表,这些参数通常是类型参数。类型参数在函数定义中用作占位符,表示将由实际的数据类型替换。

template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

实例化

函数模板的实例化可以是显式的或隐式的:

显式实例化:你指定模板参数的类型。
隐式实例化:编译器根据提供给函数的参数类型自动推导出模板参数的类型。

int main() {
    int x = 5, y = 10;
    double n = 3.5, m = 1.2;

    // 隐式实例化
    std::cout << "Max int: " << max(x, y) << std::endl;
    std::cout << "Max double: " << max(n, m) << std::endl;

    // 显式实例化
    std::cout << "Max int using explicit: " << max<int>(x, y) << std::endl;
    std::cout << "Max double using explicit: " << max<double>(n, m) << std::endl;

    return 0;
}

注意事项:

  • 模板代码在使用前不会被编译,只有在模板实例化时才进行编译。
  • 因为模板会为每种数据类型生成不同的函数实例,所以使用模板可能会增加编译后的程序大小。

具体例子

01 变量交换函数模板

template <typename T>
void Swap(T & x,T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}

02 查询数组最大值函数模板

// 求数组最大元素的MaxElement函数模板
template <class T>
T MaxElement(T a[], int size) // size是数组元素个数
{
    T tmpMax = a[0];
    for(int i = 1;i < size;++i)
    {
        if(tmpMax < a[i])
        {
            tmpMax = a[i];
        }
    }
    return tmpMax;
}

03 多个类型参数模板函数
函数模板中,可以不止一个类型的参数:

template <typename T1, typename T2>
T2 MyFun(T1 arg1, T2 arg2)
{
    cout<< arg1 << " "<< arg2<<endl;
    return arg2;
}
//T1 是传入的第一种任意变量类型,T2 是传入的第二种任意变量类型。

04 函数模板的重载
函数模板可以重载,只要它们的形参表或类型参数表不同即可。

// 模板函数 1
template<typename T1, typename T2>
void print(T1 arg1, T2 arg2) 
{
    cout<< arg1 << " "<< arg2<<endl;
}

// 模板函数 2
template<typename T>
void print(T arg1, T arg2) 
{
    cout<< arg1 << " "<< arg2<<endl;
}

// 模板函数 3
template<typename T,typename T2>
void print(T arg1, T arg2) 
{
    cout<< arg1 << " "<< arg2<<endl;
}

上面都是 print(参数1, 参数2) 模板函数的重载,因为「形参表」或「类型参数表」名字不同。
模板函数 2 和模板函数 3 看似相似,但它们有一个关键的区别,那就是在模板参数的定义上。尽管这两个模板函数的行为在某些情况下可能相同,关键的区别在于类型参数的泛化程度和模板的调用方式。
模板2:这个函数模板只使用了一个类型参数 T,这意味着 arg1 和 arg2 必须是相同的类型。只有当两个参数类型完全一致时,这个模板才会被实例化。
模板3:这个函数模板使用了两个类型参数 T 和 T2,这允许 arg1 和 arg2 是不同的类型。这种模板的灵活性更高,因为它不要求两个参数具有相同的类型

二者区别于用途
类型的灵活性:模板函数 3 比模板函数 2 提供了更高的类型灵活性,因为它允许两个不同类型的参数。这在处理不同类型的数据时非常有用。
实例化和重载解析:在函数调用时,如果提供了两个不同类型的参数,模板函数 3 将被优先选择,因为它提供了精确匹配的重载版本。如果两个参数类型相同,那么模板函数 2 和模板函数 3 都可以匹配,但通常编译器会优先选择最特化的版本,即模板参数数量最少的版本。

05 函数模板和函数的次序
在有多个函数和函数模板名字相同的情况下,编译器如下规则处理一条函数调用语句: 1. 先找参数完全匹配的普通函数(非由模板实例化而得的函数); 2. 再找参数完全匹配的模板函数; 3. 再找实参数经过自动类型转换后能够匹配的普通函数; 4. 上面的都找不到,则报错。

// 模板函数 - 1个参数类型
template <class T>
T Max(T a, T b) 
{
    cout << "TemplateMax" <<endl; return 0;
}

// 模板函数 - 2个参数类型
template <class T, class T2>
T Max(T a, T2 b) 
{
    cout << "TemplateMax2" <<endl; return 0;
}

// 普通函数
double Max(double a, double b)
{
    cout << "MyMax" << endl;
    return 0;
}

int main() 
{
    int i=4, j=5;

    // 输出MyMax - 匹配普通函数
    Max( 1.2, 3.4 ); 

    //输出TemplateMax - 匹配参数一样的模板函
    Max( i, j );

    //输出TemplateMax2 - 匹配参数类型不同的模板函数
    Max( 1.2, 3 );   

    return 0;
}

匹配模板函数时,当模板函数只有一个参数类型时,传入了不同的参数类型,是不进行类型自动转换,具体例子如下:

// 模板函数 - 1个参数类型
template<class T>
T myFunction( T arg1, T arg2)
{ 
    cout<<arg1<<" "<<arg2<<"\n"; 
    return arg1;
}

...

// OK :替换 T 为 int 类型
myFunction( 5, 7); 

// OK :替换 T 为 double 类型  
myFunction(5.8, 8.4);

// error :没有匹配到myFunction(int, double)函数
myFunction(5, 8.4);

二、类模板

提到STL标准模板库必定会提到类模板。

2.1定义类模板

类似于函数模板,类模板的代码定义:

template <typename Type> // 模板类型
template <class Type> // 模板类
  • template 告诉编译器,将要定义一个模板,尖括号中的内容相当于函数的参数列表。
  • 关键字 class 看作变量的类型名,变量接受类型作为其值,把Type看作是该变量的名称。
  • Type 表示一个通用的类型说明符。在使用模板时,使用实际的类型替换它。

如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。

模板类不是函数,不能单独编译。

模板必须和特定的模板实例化请求一起使用。一般是使将所有模板信息放在一个头文件中。
在这里插入图片描述
在这里插入图片描述

2.2 使用模板了类

声明模板类的对象,必须显式地提供所需的类型。这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定要生成哪种函数。但是类模板不行

Stack<int> kernels; // 存储int类型的栈类
Stack<string> colonels;  // 存储string类型的栈类

2.3 深入探讨模板类

可以将内置类型或类对象用作类模板Stack的类型。指针可以吗?例如,可以使用char指针替换string对象吗?
毕竟,这种指针是处理C-风格字符串的内置方式。答案是可以创建指针栈,但如果不对程序做重大修改,将无法很好地工作。编译器可以创建
类,但使用效果如何就因人而异了。
因为你虽然用指针代替string,但是你没有为其开辟内存!!!编译虽然会通过,但是运行时程序会崩溃。

使用指针栈的方法之一是,让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。把这些指针放在栈中是有意义的,因为每
个指针都将指向不同的字符串。注意,创建不同指针是调用程序的职责,而不是栈的职责。栈的任务是管理指针,而不是创建指针。
详情见书《C++ Primer Plus 6th中文版》P465

指针这块用得确实很少,所以不补充这块内容。等遇到有用的再另外填坑

2.4 数组模板示例和非类型参数

探讨探讨一些非类型(或表达式)参数以及如何使用数组来处理继承族。
首先介绍一个允许指定数组大小的简单数组模板:

  1. 一种方法是在类中使用动态数组和构造函数参数来提供元素数目。
  2. 另一种方法是使用模板参数来提供常规数组的大小,C++11新增的模板array就是这样做的。
// arraytp.h -- Array Template
#ifndef ARRAYTP_H_
#define ARRAYTP_H_

#include <iostream>
#include <cstdlib>

template <class T, int n>
class ArrayTP{
private:
	T ar[n];
public:
	ArrayTP() {};
	explicit ArrayTP(const T & v);
	virtual T & operator[](int i);
	virtual T operator[](int i) const;
};

template <class T, int n>
ArrayTP<T,n>::ArrayTP(const T & v){
	for (int i = 0; i< n; i++) ar[i] = v;
}

template <class T, int n>
T & ArrayTP<T, n>::operator[](int i){
	if (i < 0 || i >= n){
		std::cerr << "Error in array limits: " << i << " is out of range\n";
		std::exit(EXIT_FAILURE);
	}
	return ar[i]
}

template <class T, int n>
T & ArrayTP<T, n>::operator[](int i) const {
	if (i < 0 || i >= n){
		std::cerr << "Error in array limits: " << i << " is out of range\n";
		std::exit(EXIT_FAILURE);
	}
	return ar[i]
}

#endif

这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。

表达式参数方法的主要缺点是,每种数组大小都将生成自己的模板。也就是说,下面的声明将生成两个独立的类声明:

ArrayTP<double, 12> t1;
ArrayTP<double, 13> t2;

但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:

ArrayTP<int> t1(12);
ArrayTP<int> t1(13);

2.5模板多功能性

模板类的三个功能:

  • 用作基类
 template <typename Type>
 class GrowArray : public Array <Type> {...};
  • 用作组件类

    • 常规组件

      template <typename Tp>
       class Stack
      {
        Array<Tp> ar;   // use an Array<> as a component
       ...
       };
      
    • 模板可以包含多个类型参数。

      template <class T1, class T2>
      class Pair
      	{
      private:
      	T1 a;
      	T2 b;
      public:
      	T1 & first();
      	T2 & second();
       ...
      };
      
      
    
  • 递归使用模板

    ArrayTP<ArrayTP<int, 5>, 10> twodee
    // 与下面常规数组声明等价
    int twodee[10][5]
    

    在模板语法中,维的顺序与等价的二维数组相反。

  • 默认类型模板参数

    • 可以为类型参数提供默认值

      template <class T1, class T2 = int> 
      class Topo {...};
      // 如果省略了T2的值,编译器会默认使用 int
      
    • 不能为函数模板参数提供默认值。但可为非类型参数提供默认值。

2.6 模板的具体化

隐式实例化显式实例化显式具体化,统称为 具体化

模板使用 泛型 的方式描述类具体化是使用具体化的类型生成类声明

  • 隐式实例化:

    当模板函数或模板类被使用时,如果还没有相应的实例,编译器会根据提供给模板的实际类型参数自动生成一个特定的实例。

    • 声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义。

      ArrayTP <int, 100> stuff;
      
  • 显式实例化:

    这是一种手动指示编译器为特定类型生成模板实例的方法,无需等待模板的实际使用。这样做的好处是可以减少编译时间,因为编译器不需要在每次遇到新的类型时重新实例化模板。

    • 当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明的显式实例化。

    • 声明必须位于模板定义所在的名称空间中。

      template class ArrayTP<string, 100>; // 虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
      
  • 显式具体化:
    当模板的一般实现不适用于某些特定类型时,可以为这些类型提供特殊的实现。这称为模板的显式具体化,也常简称为模板特化。这使得模板在处理特定类型时可以有不同的行为。

    • 格式如下:

      template <> class Classname <specialized-type-name> {...};
      // 显式具体化的例子
      template <> class Pair<int,int> {...};
      
  • 部分具体化
    是一种特殊的模板特化形式,用于对模板的一部分参数进行具体化,而不是全部参数。部分具体化非常适用于模板类,因为它允许开发者针对某些特定情况提供更具体的实现,同时保持模板的泛用性。不过,需要注意的是,C++ 允许对模板类进行部分具体化,但对于模板函数则只能进行全特化,不能进行部分具体化。

    • 部分限制模板的通用性。

      // 通用模板
      template <class T1, class T2> class Pair {...};
      // 
      template <class T1> class Pair<T1,int> {...}; // T1 保持不变,但是T2 被具体化为 int
      
      template <typename T1, typename T2>
      class MyClass;  // 通用模板
      
      // 部分具体化:针对第二个类型参数是 int 的情况
      template <typename T1>
      class MyClass<T1, int> {
       // 实现细节
      };
      
    • 关键字 template 后面的 <>声明 的是没有被具体化的类型参数。

    • 如果多个模板可供选择,编译器将优先使用具体化程度最高的模板。

    • 如果提供的类型不是指针,则编译器将使用通用版本。

    • 如果提供的是指针,则编译器将使用指针具体化版本

理解显式实例化
显式实例化的主要目的是:

  • 减少编译时间:因为编译器不必为每个翻译单元中的每次使用重复生成模板的实例。
  • 避免链接错误:确保模板的每个实例在程序的任何地方被一致地使用。
  • 控制模板实例化:有助于控制模板代码膨胀和管理模板生成的二进制大小。

2.7 成员模板

模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。这种功能使得你的类可以具备处理不同数据类型的灵活性,而不必为每种数据类型重写方法或类。

// template member
#include <iostream>
using std::out;
using std::cin;
using std::endl;

template <typename T>
class beta
{
  private:
    template <typename V> // 
    class hold
    {
      private:
        V val;
      public:
        hold(V v = 0) : val(v) {}
        void show() const {cout << val << endl; }
        V value() const {return val;}
    };
    hold<T> q;    // template object
    hold<int> n; // template object
  public:
    beta(T t, int i) : q(t), n(i) {}
    template<typename U> // template method
    U blab(U u,T t) {return (n.Value() + q.Value() * u / t;}
    void Show() const {q.Show(); n.Show();}
};

hold模板是在私有部分声明的,因此只能在beta类中访问它。beta类使用hold模板声明了两个数据成员:

hold<T> q;    // template object
hold<int> n; // template object

blab( )方法的U类型由该方法被调用时的参数值显式确定,T类型由对象的实例化类型确定。

模板可以嵌套,所以语法为:

template <typename T>
  template <typename V>
// 不能使用如下语法
template<typename T, typename V>

嵌套模板参数(template <typename T> template <typename V>)通常用于类或结构体内部,其中模板成员需要额外的类型参数。
多类型模板参数(template<typename T, typename V>)用于定义同时需要多个类型参数的函数或类。

2.8 使用模板作为参数

模板可以包含类型参数(如typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性,用于实现STL。

template <template <typename T> class Thing>
class Grab
  • 模板参数是 template<typename T> class Thing,其中template class 是类型,Thing 是参数。

以下是一个示例,演示如何使用模板作为参数来设计一个容器包装器类,该类可以与不同的容器类型一起工作,如 std::vector、std::list 等。
我们将创建一个 ContainerWrapper 类,它接受两个参数:一个是元素类型,另一个是容器类型。容器类型本身是一个模板,这就是模板模板参数的使用场景。

#include <iostream>
#include <vector>
#include <list>

// 定义模板模板参数
template <template <typename, typename> class ContainerType, typename ValueType, typename Allocator = std::allocator<ValueType>>
class ContainerWrapper {
public:
    ContainerType<ValueType, Allocator> container;

    void add(const ValueType& value) {
        container.push_back(value);
    }

    void display() const {
        for (auto& elem : container) {
            std::cout << elem << ' ';
        }
        std::cout << std::endl;
    }
};

int main() {
    // 使用 vector 作为容器
    ContainerWrapper<std::vector, int> vectorWrapper;
    vectorWrapper.add(10);
    vectorWrapper.add(20);
    vectorWrapper.add(30);
    vectorWrapper.display();  // 输出: 10 20 30 

    // 使用 list 作为容器
    ContainerWrapper<std::list, int> listWrapper;
    listWrapper.add(40);
    listWrapper.add(50);
    listWrapper.add(60);
    listWrapper.display();  // 输出: 40 50 60
}

2.9 模板类和友元

模板类声明也可以友元。模板的友元分为3类:

  • 非模板友元

    • 在模板类中声明的一个常规友元函数,称为模板所有实例化的友元。例如,它将是类hasFriend和HasFriend的友元。
    template<class T>
    class HasFriend
    {
     public:
     friend void counts();
    	...
    };
    
    • 当为友元函数提供模板类参数,必须指明具体化。
    template <class T>
    class HasFriend
    {
     friend void report(HasFriend<T> &);// 具体化之后,就会变成约束模板友元
    };
    
  • 约束模板友元:友元的类型取决于类被实例化时的类型。

    • 类外声明的模板具体化

    • 2个步骤:

      • 在类定义的前面声明每个模板函数
      template <typename T> void counts();
      template <typename T> void reports(T &);
      
      • 在函数中再次将模板声明为友元。根据类模板参数的类型声明具体化。
      template <typename TT>
      class HasFriendT
      {
      friend void counts<TT>();
      friend void report <HasFriendT<TT> &>;
       ...
      };
      
  • 模板类的非约束模板友元函数:即友元的所有具体化都是类的每一个具体化的友元。
    类内部声明模板,可以创建非约束友元函数

template <typename T>
class ManyFriend
{
  template <typename C,typename D> friend void show2(C &, D &)
  ...
};
// 实现使用方式
template <typename C, typename D> void show2(C & c, D & d)
{
  ...
}

2.10 模板别名(C++11)

  • 使用 typedef 为模板具体化指定别名
typedef std::array<double,12> arrd;
arrd gallons; // gallon type : std::array<double,12>
  • 使用 using name = type 于模板
template <typename T>
using arrtype = std::array<T,12>; // arrtype定义为一个模板别名

arrtype<double> gallons;   //gallons is type std::array<double, 12>

using用于非模板时与常规的typedef等价。


总结

  1. 函数模板
  2. 类模板:基本上就是围绕着如何实例化展开,以及一些模板的高级用法

参考文献

  1. 《C++ Primer Plus 6th中文版》
  2. https://github.com/SolerHo/cpp-Primer-Plus-6e-Notes/tree/master
  3. C++ 模板常见特性(函数模板、类模板)(小林coding)
  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值