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&
    r

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++岗面试真题解析 文章被收录于专栏

<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>

全部评论
这个萃取有点没看懂
2
送花
回复
分享
发布于 2022-06-09 11:48
感谢大佬的讲解,但是 萃取部分 if (!TypeTraits<t2>().traits.get()),好像应该把!去掉</t2>
点赞
送花
回复
分享
发布于 2023-06-07 14:26 浙江
秋招专场
校招火热招聘中
官网直投
萃取这块代码应该写错了,判断条件是TypeTraits<t2>().traits.get(),为了判断T2类型是float或double,注释也是错的</t2>
点赞
送花
回复
分享
发布于 2023-08-22 00:07 北京

相关推荐

点赞 评论 收藏
转发
点赞 2 评论
分享
牛客网
牛客企业服务