2-3 模版与泛型编程

1. 前言

我们知道声明变量时需要指明变量的类型,类型决定了变量的属性和表现。变量的类型可以是内置的类型,例如int、float、double等;也可以是自定义的类型,例如结构体和类。在2-2章中函数重载一节我们了解了同名函数、不同数量或类型的参数构成了函数重载的条件,编译器在调用重载函数时根据参数类型推导出最适配的函数调用。

但如果我们想实现一个函数,它的参数类型有很多种可能,那么为了适配所有的参数类型,我们需要编写很多很多个重载的函数。

那么能不能不指定函数参数的类型,即参数类型以泛型的型式定义呢?

答案是:能,C++支持编写函数模板和类模板,以支持函数和类的多样性。STL标准库中容器和方法基本都以模板的方式实现。

函数重载和模板均是静态多态的实现方式,此外在2-4章中详细介绍了可变参数模板,读者可在阅读到相应内容时进行呼应。

2. 函数模板

在不使用模板的情况下,我们定义一个函数需要按如下的方式声明:ret_type function_name (parameter list),即 返回值类型 函数名(参数列表)。当定义模板时,需要先声明变量类型的占位符,语法如下:

template <typename T>
或者
template <class T>
ret_type function_name (parameter list)

T是变量类型的占位符,在函数声明前增加模板定义,即可以在这个函数实现的任何位置使用T类型去定义变量,但需要注意的是T作为泛型参数时无法制定默认值(形参不能为空)。
举例,声明泛型T和模板函数,返回值为T类型,参数为两个T类型的const引用,函数实现为两个参数相加:

#include<iostream>
#include<string>
using namespace std;

template <typename T>
T add(const T& a, const T& b)
{
    return a + b;
}

int main()
{
    int i = 1, j = 2;
    cout << "i + j = " << add(i, j) << endl;  // i + j = 3
    double x = 3.14, y = 5.27;
    cout << "x + y = "<< add(x, y) << endl;  // x + y = 8.41
    string s1 = "hello";
    string s2 = "world";
    cout << "s1 + s2 = "<<add(s1, s2)<<endl; // s1 + s2 = helloworld
    return 0;
}

通过函数模板中占位符的类型在函数实际调用时被指定,从而实现了一个接口根据不同类型的入参实现不同的功能——接口重用。

3. 类模版

与函数模板类似的,在类定义前声明泛型占位符,在类的定义中使用占位符去定义变量。当类在实例化对象时,需要指定模版中占位符的实际类型,例如:
定义一个模板类,它有两个泛型占位符,在类中定义STL中的map容器作为成员变量,并实现向map中插入数据和将map打印的方法。

#include <map>
#include <string>
#include <iostream>
using namespace std;
template <typename T1, typename T2>
class KVMap
{
private:
    map<T1, T2> kvMap;
public:
    void setValue(const T1 key, const T2 value)  // 向map中插入数据
    {
        kvMap.insert(make_pair(key, value));
    }
    void printMap()     // 打印map的key和value
    {
        for(auto iter = kvMap.begin(); iter != kvMap.end(); iter++)
        {
            cout<<iter->first<<"="<<iter->second<<"&";
        }
        cout<<endl;
    }
};

int main()
{
    KVMap<string, string> kvParams;  // 实例化对象时指明模板的类型
    kvParams.setValue("age", "25");
    kvParams.setValue("name", "Evila");
    kvParams.printMap();  // age=25&name=Evila&
    return 0;
}

3.1 类模板继承于类模板

类模板可以作为基类被继承,若派生类也为类模板,那么可以指定基类特定的类型,也可以使用派生类的泛型来指定基类。

// 以上节定义的类模板为基类
template <typename T1, typename T2>
class KVMap;

// 若派生类也为类模板,则可以用派生类的泛型来指定基类
template <typename T1, typename T2>
class KVMapOne : public KVMap<T1, T2> 
{
}

3.2 普通类继承于类模板

类模板可以作为基类被继承,若派生类为普通类,那么必须指明当前基类的类型。

// 以上节定义的类模板为基类
template <typename T1, typename T2>
class KVMap;

// 若派生类也为类模板,则可以用派生类的泛型来指定基类
class KVMapTwo : public KVMap<std::string, int> 
{
}

3.3 类模板继承于普通类

类模板继承于普通类时,与普通的继承并无区别。类模板作为派生类,根据继承权限获得基类中的成员访问。

4. 模板特化、偏特化与萃取机

4.1 模板全特化

通过上节可以认识到,模板在非特化情况下,占位符的类型是在实例化时指明的。但有时在实现模板类或模板函数时,需要对某个确定的类型进行特殊处理,即实现特定类型下的非通用行为。此时,我们需要对模板进行特化处理,若将所有的占位符特化为绝对类型,则视为模板的全特化。

举个例子:
1、首先定义一个模板类,这里直接引用第二节定义的KVMap模板类。
2、对模板中的两个占位符进行全特化实现,如特化为string和double。实现非通用行为:在打印double的值时,保留3位数字。

template <> // 不能省略,为了说明正在实现该类的特化版本
class KVMap <string, double>
{
private:
    map<string, double> kvMap;
public:
    void setValue(const string key, const double value)  // 向map中插入数据
    {
        kvMap.insert(make_pair(key, value));
    }
    void printMap()     // 打印map的key和value
    {
        for(auto iter = kvMap.begin(); iter != kvMap.end(); iter++)
        {
            cout<<iter->first<<"="<<setprecision(3)<<iter->second<<"&";  // value为double类型时,只输出3位长度
        }
        cout<<endl;
    }
};
int main()
{
    KVMap<string, double> kvParams;
    kvParams.setValue("age", 25.12345); 
    kvParams.printMap();  // age=25.1&
    return 0;
}

4.2 模板偏特化

若只对模板声明中的部分占位符进行特化处理,或对全部模板占位符进行修饰处理,则可称为模板的偏特化。

  • 偏特化:特化部分占位符
    #include <map>
    #include <iostream>
    #include <iomanip>
    using namespace std;
    template <typename T>
    class KVMap <string, T>  // 该类具有两个模板占位符,一个被特化为string,第二个未特化
    {
    private:
      map<string, double> kvMap;
    public:
      void setValue(const string key, const T value)  // 向map中插入数据
      {
          kvMap.insert(make_pair(key, value));
      }
      void printMap()     // 打印map的key和value
      {
          for(auto iter = kvMap.begin(); iter != kvMap.end(); iter++)
          {
              cout<<iter->first<<"="<<setprecision(3)<<iter->second<<"&";  // 只输出3位数字
          }
          cout<<endl;
      }
    };
    int main()
    {
      KVMap<string, double> kvParams; // 实例化时确定模板类型
      kvParams.setValue("age", 25.12345);  
      kvParams.printMap();  // age=25.1&
      return 0;
    }
  • 偏特化:特化为指针、引用、其他模板类型,这类偏特化对模板做了某些限定,但仍保留了泛型的通用性。
    template <typename T1, typename  T2>
    class KVMap <T1 *,T2 *>
    {
    private:
      map<T1*, T2*> kvMap;
    public:
      void setValue(T1* key, T2* value)  // 向map中插入数据
      {
          kvMap.insert(make_pair(key, value));
      }
      void printMap()     // 打印map的key和value
      {
          for(auto iter = kvMap.begin(); iter != kvMap.end(); iter++)
          {
              // key和value被偏特化为指针类型,输出变值时需要解引用
              cout<<(*iter->first)<<"="<<setprecision(3)<<(*iter->second)<<"&";
          }
          cout<<endl;
      }
    };
    int main()
    {
      KVMap<string*, string*> kvParams; // 实例化时确定模板类型
      string key1 = "age";
      string value1 = "100";
      kvParams.setValue(&key1, &value1);
      kvParams.printMap(); // age=100&
      return 0;
    }
    // C++不支持模板函数的偏特化,事实上对模板函数进行重载可以视为"偏特化"。例如:
    template <typename T1, typename  T2>
    void func(T1 t1, T2 t2);    // 未特化的模板函数
    template <typename T1>
    void func(T1 t1, int t2);   // 同名函数,第二个参数为int类型,这其实是函数的重载

4.3 类型萃取机制

泛型类和方法在模板的支持下,可以支持多种多样的变量类型。若我们在编写模板类或模板函数时,对某个特殊类型需要实现特殊处理,可以选择性的使用全特化和偏特化实现。但如果模板类和模板函数需要实现很多不同类型的特殊性质,那么就需要编写多个特化副本。试想一下若有一个萃取机可以帮助判断传入的类型,那么就可以在一份实现中编写多个分支,去实现不同的特性。

总结概念:萃取是在模板类中萃取类型的某些特性,帮助判断该类型是否需要实现某些特性,从而在泛型方法中来对该类型进行特殊的处理的机制。

萃取机制的实现依靠于模板特化机制和typedef类型定义关键字。
举个例子:在讲述类模板案例中实现了模板类KVMap,类中依靠成员变量std::map容器实现了成员函数printMap()。如果我们希望在打印map中存储的数据时,对于value为float和double类型实现只保留3位小数的特性,就需要在打印函数中利用类型萃取机制来获得value的变量类型,从而做出特性动作。

#include <iostream>
#include <map>
#include <iomanip>
#include <string>
#include <vector>
using namespace std;

// 1. 首先实现两个自定义类型,分别代表 '是' float/double类型 和 '不是'float/double类型
struct __TrueFloatOrDoubleType  // '是'float/double类型
{
    bool get()
    {
        return true;   // 判断是float或double类型
    }
};
struct __FalseFloatOrDoubleType  // '不是'float/double类型
{
    bool get()
    {
        return false;  // 判断不是float或double类型
    }
};

// 2. 定义萃取机类型
template <class _Tp>
struct TypeTraits
{
    typedef __FalseFloatOrDoubleType   __IsFloatOrDoubleType;  // 对类型重命名,默认定义不是float或double类型
    __IsFloatOrDoubleType traits;
};
// 3. 对萃取机类实现特化是类型萃取的关键
template <>
struct TypeTraits<float>  // 萃取机模板特化类型为float
{
    typedef __TrueFloatOrDoubleType   __IsFloatOrDoubleType;  // 对类型重命名,定义为是float/double类型
    __IsFloatOrDoubleType traits;
};
template <>
struct TypeTraits<double>  // 萃取机模板特化类型为double
{
    typedef __TrueFloatOrDoubleType   __IsFloatOrDoubleType;  // 对类型重命名,定义为是float/double类型
    __IsFloatOrDoubleType traits;
};
// 4. KVMap中利用萃取机制在泛型方法中实现特性
template <typename T1, typename  T2>
class KVMap
{
private:
    map<T1, T2> kvMap;
public:
    void setValue(T1 key, T2 value)  // 向map中插入数据
    {
        kvMap.insert(make_pair(key, value));
    }
    void printMap()     // 打印map的key和value
    {
        for(auto iter = kvMap.begin(); iter != kvMap.end(); iter++)
        {
            // 这里用萃取机实现判断T2是否为float或double类型
            if (!TypeTraits<T2>().traits.get())
            {
                // 5. 关键点:TypeTraits<T2> 若T2是float或double,则编译器会选择TypeTraits的特化实现
                // 在TypeTraits的特化实现中,我们将__IsFloatOrDoubleType由typedef定义
                //__FalseFloatOrDoubleType得到,因此get()方***返回false
                // 在这个if分支中,T2的类型一定是float或double 因此打印时只输出3位小数
                cout<<iter->first<<"="<<setprecision(3)<<iter->second<<"&";
            }
            else
            {
                 // T2不是float或doube类型,打印时全部输出
                cout<<iter->first<<"="<<iter->second<<"&";
            }
        }
        cout<<endl;
    }
};
int main()
{
    KVMap<string, double> kvParams; // 实例化时确定模板类型
    string key1 = "age";
    double value1 = 100.12345;  
    kvParams.setValue(key1, value1);
    kvParams.printMap();  // age=100.123&
    return 0;
}

以上用一个小的案例介绍了萃取机制在模板类编写过程中的作用,事实上萃取机制在STL标准库中被广泛应用。STL标准库中提供了一系列容器模板和泛型方法,而容器迭代器是容器与方法之间沟通的桥梁,泛型方法希望了解到传入容器的数据类型、数据指针和引用类型、迭代器类型、元素间隔计算类型等。因此,对容器迭代器进行类型萃取是实现STL中泛型方法的重要一步。

// 在GNU2.9中关于迭代器萃取机iterator_traits的声明,iterator_traits能够根据传入的迭代器类型萃取出以下5种有关容器的类型。
template <class _Iterator>
struct iterator_traits {
  typedef typename _Iterator::iterator_category iterator_category;  // 迭代器的类型
  typedef typename _Iterator::value_type        value_type;     // 数据类型
  typedef typename _Iterator::difference_type   difference_type; // 距离类型   
  typedef typename _Iterator::pointer           pointer;     // 指针类型
  typedef typename _Iterator::reference         reference;    // 引用类型
};
全部评论

相关推荐

10-30 23:23
已编辑
中山大学 Web前端
去B座二楼砸水泥地:这无论是个人素质还是专业素质都👇拉满了吧
点赞 评论 收藏
分享
10-15 15:00
潍坊学院 golang
跨考小白:这又不是官方
投递拼多多集团-PDD等公司10个岗位
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务