跳过正文
P14 模板与泛型
  1. S/
  2. C/C++/
  3. C/
  4. C语言程序设计/

P14 模板与泛型

Li
作者
Li
往前走,别回头!
目录
C 语言程序设计笔记 - 这篇文章属于一个选集。

知识点
#

15.1 模板概念与函数模板的定义、调用
#

  • @ 函数模板图
    • ![[白板/P44 函数模板.canvas|P44 函数模板]]

15.1.1 模板概念
#

[!tip] 模板与泛型

  • @ 泛型编程: 以独立于任何特定类型的方式编写代码
  • @ 模板: 泛型编程的基础,是创建类或者函数的蓝图或者公式
  • 模板支持将==类型作为参数==的程序设计方式
  • 模板一般分为函数模板类模板
  • 通过给模板提供足够的信息,让蓝图转变为具体的类或函数,这种转变发生在==编译时==

15.1.2 函数模板的定义
#

[!tip] 函数模板定义

  • @ 函数模板: 以 template 关键字开头,后面跟尖括号中的模板参数列表
  • 模板参数用 typenameclass 修饰(两者等价,与类定义的 class 无关)
  • 多个模板参数用逗号分隔
  • 模板参数列表中的 T 是类型参数,具体代表什么类型由编译器根据调用推断
template <typename T>
T funcadd(T a, T b) {
    T addhe = a + b;
    return addhe;
}

15.1.3 函数模板的调用
#

[!tip] 函数模板调用

  • 调用时编译器根据实参推断模板参数类型
  • 推断成功后,编译器实例化一个特定版本的函数
  • ! 如果实参类型不一致(如 intfloat),编译器无法推断,报错
  • 可以用 < > 显式指定模板参数
int he = funcadd(3, 4);           // 推断 T 为 int
float he2 = funcadd(3.1f, 1.2f);  // 推断 T 为 float
int he3 = funcadd<int>(3, 4);     // 显式指定

15.1.4 非类型模板参数
#

[!tip] 非类型模板参数

  • @ 非类型模板参数: 表示一个值而非类型,用传统类型名指定(如 int size
  • ! 非类型模板参数必须是==常量表达式==,值在编译时确定
  • 可以与类型模板参数混合使用
  • 显式指定模板参数时,以 < > 中提供的类型为准
  • 函数模板的 inline 写在模板参数列表之后
template <int a, int b>
int funcaddv2() {
    int addhe = a + b;
    return addhe;
}
int result = funcaddv2<12, 13>();  // 通过 <> 传递常量

template <typename T, int a, int b>
int funcaddv3(T c) {
    int addhe = (int)c + a + b;
    return addhe;
}
int result = funcaddv3<int, 11, 12>(13);  // 混合使用

[!tip] 模板参数分类

分类说明值来源
类型模板参数typename/class 修饰编译器推断或用户提供
非类型模板参数传统类型名修饰用户提供(常量表达式)

15.2 类模板概念与类模板的定义、使用
#

  • @ 类模板图
    • ![[白板/P45 类模板.canvas|P45 类模板]]

15.2.1 类模板概念
#

[!tip] 类模板概念

  • @ 类模板: 产生类的模具,通过给定的模板参数生成具体的类
  • ! 编译器==不能为类模板推断模板参数==,必须用 < > 提供额外信息
  • 类模板名不是类型名,实例化后的才是(如 myvector<int> 才是类型名)
  • 每种类型编译器都会生成一个不同的类

15.2.2 类模板的定义
#

[!tip] 类模板定义

  • 类模板的所有信息(声明和实现)都必须写在一个 .h 文件中
  • 因为实例化具体类时必须有类模板的全部信息
  • 其他源文件只需 #include 这个 .h 文件即可
// myvector.h
template <typename T>
class myvector {
public:
    typedef T* iterator;
public:
    myvector();  // 构造函数声明
    myvector& operator=(const myvector&);  // 赋值运算符
public:
    iterator mybegin();
    iterator myend();
};

15.2.3 类模板的成员函数
#

[!tip] 类模板成员函数

  • 在类模板定义内部实现的成员函数被隐式声明为内联函数
  • 在类模板定义外部实现时:
    • 1 必须以 template 开头,后接模板参数列表
    • 2 类名后用 < > 列出所有模板参数名
  • ! 只有被使用到的成员函数才会被实例化
// 类模板外部实现
template <typename T>
void myvector<T>::myfunc() { /* ... */ }

template <typename T>
myvector<T>::myvector() { /* ... */ }

15.2.4 类模板名字的使用
#

[!tip] 类模板名字使用规则

  • 在类模板定义内部,可以直接使用类模板名,不需要跟模板参数
  • 编译器会假定 myvector 等价于 myvector<T>
  • ! 在类模板定义之外,必须带上模板参数
// 类内部
myvector& operator=(const myvector&);  // 可以

// 类外部
template <typename T>
myvector<T>& myvector<T>::operator=(const myvector<T>&) {
    return *this;
}

15.2.5 非类型模板参数的使用
#

[!tip] 类模板非类型模板参数

  • 非类型模板参数可以给默认值
  • 使用默认值时用空的 < >(不能省略)
  • ! 浮点型和类类型==不能==作为非类型模板参数
template <typename T, int size = 10>
class myarray {
private:
    T arr[size];
};

myarray<int, 100> tmparr;   // 指定 size=100
myarray<int> tmparr;         // 使用默认 size=10

// 类外部实现
template <typename T, int size>
void myarray<T, size>::myfunc() { /* ... */ }

15.3 使用 typename 的场合、函数模板、默认模板参数与趣味写法分析
#

15.3.1 typename 的使用场合
#

[!tip] typename 两种用法

  • % 用法一:在模板定义里,表明其后的模板参数是类型参数
    • template <typename T> 中的 typename 可替换为 class
  • % 用法二:用 typename 标明这是一个类型(类型成员)
    • ! 在类模板定义之外,访问依赖于模板参数的类型成员时,必须加 typename
    • 默认情况下 C++ 假定通过 :: 访问的是静态成员变量而非类型
    • ! 此处 typename 不能用 class 替换
// 用法二:标明类型成员
template <typename T>
typename myvector<T>::myiterator myvector<T>::mybegin() { /* ... */ }

// 另一个范例
template <typename T>
typename T::size_type getlength(const T& c) {
    if (c.empty()) return 0;
    return c.size();
}

15.3.2 函数指针作为其他函数的参数
#

[!tip] 函数指针作参数

  • 使用 typedef 定义函数指针类型
  • 将函数指针类型作为函数形参类型
typedef int(*FuncType)(int, int);

void testfunc(int i, int j, FuncType funcpoint) {
    int result = funcpoint(i, j);
    cout << result << endl;
}

testfunc(3, 4, mf);  // mf 是函数名,代表函数首地址

15.3.3 函数模板趣味用法举例
#

[!tip] 可调用对象与函数模板

  • @ 可调用对象: 重载了 () 运算符的类的对象,可以像函数一样调用
  • 同一个函数模板,根据传递参数不同,可推断出不同类型:
    • 函数指针类型
    • 可调用对象类型
  • ! 可调用对象必须重载 () 运算符,且参数和返回值类型需匹配
template <typename T, typename F>
void testfunc(const T& i, const T& j, F funcpoint) {
    cout << funcpoint(i, j) << endl;
}

// 可调用对象类
class tc {
public:
    int operator()(int v1, int v2) const { return v1 + v2; }
};

tc tcobj;
testfunc(3, 4, tcobj);    // 推断 F 为 tc 类类型
testfunc(3, 4, tc());     // 传递临时对象,节省拷贝构造

15.3.4 默认模板参数
#

[!tip] 默认模板参数

  • 类模板和函数模板都可以有默认模板参数
  • ! 从有默认值的参数开始,后面的所有参数都得有默认值
  • 类模板使用默认值时用空的 < >(不能省略)
  • C++11 新标准允许为函数模板提供默认模板参数
// 类模板默认参数
template <typename T = string, int size = 5>
class myarray { /* ... */ };

myarray<> abc;       // 完全使用默认值
myarray<int> def;     // 只提供第一个参数

// 函数模板默认参数
template <typename T, typename F = tc>
void testfunc(const T& i, const T& j, F funcpoint = F()) {
    cout << funcpoint(i, j) << endl;
}
testfunc(3, 4);  // 使用默认模板参数和默认函数参数

15.4 成员函数模板、模板显式实例化与声明
#

15.4.1 普通类的成员函数模板
#

[!tip] 普通类的成员函数模板

  • 普通类的成员函数本身可以是一个函数模板
  • ! 成员函数模板==不可以是虚函数==
class A {
public:
    template <typename T>
    void myft(T tmpt) {
        cout << tmpt << endl;
    }
};

A a;
a.myft(3);  // 自动推断 T 为 int

15.4.2 类模板的成员函数模板
#

[!tip] 类模板的成员函数模板

  • 类模板和其成员函数模板有各自独立的模板参数
  • 在类模板外部实现时,先写类模板参数列表,再写成员函数模板参数列表
  • 构造函数也可以引入自己的模板参数,与类的模板参数无关
template <typename C>
class A {
public:
    template <typename T2>
    A(T2 v1, T2 v2);  // 构造函数模板
    template <typename T>
    void myft(T tmpt);
};

// 类外部实现
template <typename C>        // 类模板参数
template <typename T2>       // 构造函数模板参数
A<C>::A(T2 v1, T2 v2) { /* ... */ }

15.4.3 模板显式实例化与声明
#

[!tip] 模板显式实例化

  • 多个 .cpp 文件独立编译时,可能重复实例化相同模板,增加编译开销
  • @ 实例化定义: template A<float>; — 让编译器实例化,只写在一个 .cpp
  • @ 实例化声明: extern template A<float>; — 告诉编译器其他文件已实例化,不重复生成
  • 函数模板同理:template void myfunc(int&, int&);
  • ! 实例化定义只有一个,实例化声明可以有多个
  • ! 显式实例化会把类模板所有成员函数都实例化出来
// test.cpp 中(实例化定义)
template A<float>;
template void myfunc(int&, int&);

// MyProject.cpp 中(实例化声明)
extern template A<float>;
extern template void myfunc(int&, int&);

15.5 using 定义模板别名与显式指定模板参数
#

15.5.1 using 定义模板别名
#

[!tip] using 定义模板别名

  • typedef 只能给固定类型起别名,无法定义类型不固定的别名
  • C++11 的 using 可以定义别名模板(类型相关的模板)
  • using 包含了 typedef 的所有功能,语法顺序相反
  • 别名模板既不是类模板也不是函数模板,是一种新的模板形式
// typedef 定义固定类型别名
typedef std::map<std::string, int> map_s_i;

// using 定义固定类型别名(语法顺序相反)
using map_s_i = std::map<std::string, int>;

// using 定义模板别名(typedef 做不到)
template <typename T>
using str_map_t = std::map<std::string, T>;

str_map_t<int> map1;        // 等价于 std::map<std::string, int>
str_map_t<string> map2;     // 等价于 std::map<std::string, string>

// using 定义函数指针类型
using FunType = int(*)(int, int);  // 比 typedef 更直观

// using 定义函数指针模板别名
template <typename T>
using myfunc_t = int(*)(T, T);

15.5.2 显式指定模板参数
#

[!tip] 显式指定模板参数

  • 显式模板实参从左到右按顺序与模板参数匹配
  • 能推导出来的可以省略,但省略后后面的也得省略
  • ! 无法推断的模板参数必须显式提供
template <typename T1, typename T2, typename T3>
T1 sum(T2 i, T3 j) {
    T1 result = i + j;
    return result;
}

auto result = sum<double, double, double>(2000000000, 2000000000);  // 三个都指定
auto result = sum<double>(2000000000, 2000000000);  // 只指定 T1

15.6 模板全特化与偏特化(局部特化)
#

  • @ 模板特化图
    • ![[白板/P46 模板特化.canvas|P46 模板特化]]

15.6.1 类模板特化
#

[!tip] 泛化与特化

  • @ 泛化: 模板可以指定任意模板参数,这就是泛化
  • @ 特化: 针对某种独特类型做单独设计和代码编写
  • ! 先有泛化版本才能有特化版本

[!tip] 类模板全特化

  • @ 全特化: 所有类型模板参数都用具体类型代替
  • 格式:template <> struct TC<int, int> { /* ... */ };
  • 特化版本具有==优先被选择权==
  • 可以有任意多个全特化版本
  • 也可以只特化某个成员函数
// 泛化版本
template <typename T, typename U>
struct TC {
    TC() { cout << "TC泛化版本构造函数" << endl; }
    void functest() { cout << "TC泛化版本" << endl; }
};

// 全特化版本
template <>
struct TC<int, int> {
    TC() { cout << "TC<int,int>特化版本构造函数" << endl; }
    void functest() { cout << "TC<int,int>特化版本" << endl; }
};

// 特化成员函数
template <>
void TC<double, double>::functest() {
    cout << "TC<double,double>的functest()特化版本" << endl;
}

[!tip] 类模板偏特化(局部特化)

  • % 模板参数数量上的偏特化: 绑定部分模板参数,留下部分
  • % 模板参数范围上的偏特化: 将类型范围缩小(如 Tconst TT*T&T&&
  • 偏特化完了本质上还是一个模板
// 数量上的偏特化
template <typename T, typename U, typename W>
struct TCP { /* 泛化 */ };

template <typename U>
struct TCP<int, U, double> { /* 偏特化:绑定第1、3个参数 */ };

// 范围上的偏特化
template <typename T>
struct TCF { /* 泛化 */ };

template <typename T>
struct TCF<const T> { /* const 特化 */ };

template <typename T>
struct TCF<T*> { /* 指针特化 */ };

template <typename T>
struct TCF<T&> { /* 左值引用特化 */ };

template <typename T>
struct TCF<T&&> { /* 右值引用特化 */ };

15.6.2 函数模板特化
#

[!tip] 函数模板全特化

  • 格式:template <> void tfunc<int, double>(int&, double&) { /* ... */ }
  • 全特化等价于实例化一个函数模板,不等价于函数重载
  • ! 编译器选择优先级:普通函数 > 函数模板特化版本 > 函数模板泛化版本
  • ! 函数模板==不能偏特化==,只有类模板才能偏特化
// 泛化版本
template <typename T, typename U>
void tfunc(T& tmprv, U& tmprv2) {
    cout << "tfunc泛化版本" << endl;
}

// 全特化版本
template <>
void tfunc<int, double>(int& tmprv, double& tmprv2) {
    cout << "tfunc<int,double>特化版本" << endl;
}

15.6.3 模板特化版本放置位置建议
#

[!tip] 特化版本放置位置

  • 模板特化版本应和模板泛化版本放在同一个 .h 文件中
  • 特化版本一般放在泛化版本的后面

15.7 可变参模板与模板模板参数
#

  • @ 可变参模板图
    • ![[白板/P47 可变参模板.canvas|P47 可变参模板]]

15.7.1 可变参函数模板
#

[!tip] 可变参函数模板

  • @ 可变参模板: C++11 引入,允许模板定义中含有 0 到多个模板参数
  • ...(省略号)表示可变参
  • typename... T — 可变参类型(一包类型)
  • T... args — 可变形参(一包形参)
  • sizeof...(args)sizeof...(T) — 获取参数个数
template <typename... T>
void myfunct1(T... args) {
    cout << sizeof...(args) << endl;  // 参数个数
}

myfunct1();                        // 0
myfunct1(10, 20);                  // 2
myfunct1(10, 25.8, "abc", 68);     // 4,类型各不相同

[!tip] 参数包展开

  • 通过递归函数方式展开参数包
  • 需要两个函数:
    • 1 参数包展开函数(函数模板):一个单独参数 + 一包参数
    • 2 递归终止函数(普通函数):0 个参数的同名函数
  • 每次调用剥离一个参数,直到参数包为空时调用终止函数
// 递归终止函数
void myfunct2() {
    cout << "参数包展开时执行了递归终止函数" << endl;
}

// 参数包展开函数
template <typename T, typename... U>
void myfunct2(const T& firstarg, const U&... otherargs) {
    cout << "收到的参数值为" << firstarg << endl;
    myfunct2(otherargs...);  // 递归调用
}

myfunct2(10, "abc", 12.7);
// 输出: 10 → abc → 12.7 → 递归终止函数

15.7.2 可变参类模板
#

[!tip] 可变参类模板

  • 参数包展开方式与函数模板不同
  • % 方式一:通过递归继承方式展开
  • % 方式二:通过递归组合方式展开
  • % 方式三:通过 tuple 和递归调用展开

[!tip] 递归继承方式

  • 偏特化版本继承参数包剩余部分
  • 每次继承拆分出一个参数
  • 需要一个 0 个模板参数的特化版本作为终止
  • 所有子部分共享同一个 this 地址
// 主模板定义(泛化版本)
template <typename... Args>
class myclasst {
public:
    myclasst() { printf("泛化版本\n"); }
};

// 特殊特化版本(0个参数,递归终止)
template <>
class myclasst<> {
public:
    myclasst() { printf("特殊特化版本\n"); }
};

// 偏特化版本(递归继承)
template <typename First, typename... Others>
class myclasst<First, Others...> : private myclasst<Others...> {
public:
    First m_i;
    myclasst(First parf, Others... paro) : m_i(parf), myclasst<Others...>(paro...) {}
};

[!tip] 递归组合方式

  • 将继承改为成员变量(组合关系)
  • 产生多个不同的对象(this 地址不同)

[!tip] tuple 与递归调用方式

  • 使用 std::tuple 存储参数
  • 通过计数器从 0 递增,用 get<计数器>(tuple) 提取每个参数
  • 需要偏特化版本作为递归终止条件
tuple<float, int, int> mytuple(12.5f, 100, 52);
cout << get<0>(mytuple) << endl;  // 12.5
cout << get<1>(mytuple) << endl;  // 100
cout << get<2>(mytuple) << endl;  // 52

15.7.3 模板模板参数
#

[!tip] 模板模板参数

  • @ 模板模板参数: 模板参数本身又是一个模板
  • 写法:template<class> class Container
  • 也可以写成 template<typename W> typename ContainerW 可省略)
  • ! 不能用 typename Container 代替模板模板参数
  • 使用 using 定义模板别名解决容器分配器参数不匹配问题
template <typename T, template<class> class Container>
class myclass {
public:
    Container<T> myc;  // Container 作为类模板使用
};

// 解决 vector 分配器参数不匹配
template <typename T>
using MYVec = vector<T, allocator<T>>;

myclass<int, MYVec> myvecobj;  // 编译通过
Reply by Email
C 语言程序设计笔记 - 这篇文章属于一个选集。

相关文章