编译器并不会为一个单纯的模板生成代码,而只有实例化一个模板才会生成实际的类,而不同的实例化之间毫无关系。既然如此函数的定义与实现自然不能够分成.h和.cpp文件来使用(隐式实例化)

当然,C++是自由的,仍然有方法能够实现分文件,即显示实例化,即在定义模板之后便对其进行指定类型的实例化,供给其他文件调用。这个特点也可以用在静态库的导出中,模板本身是没有办法像普通函数被导入静态库的二进制代码中的,需要首先进行显式实例化

与静态库相对的是动态库的导出,在静态库中,连接器仅仅是简单的进行代码的链接,而在动态库中需要有符号导出表,运行时的动态链接,二进制接口(ABI)。因此有以下几种情况

  • 仅在头文件中定义,动态库的作者和使用者都#include
    代码在主程序直接实例化,动态库的二进制文件没有这段代码,而代码逻辑被拷贝到每一个调用他的exe/so中。
  • 进行了显示实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在动态库的 .h 中
#ifdef EXPORT_DLL
#define DLL_API __declspec(dllexport) // Windows
#else
#define DLL_API __declspec(dllimport)
#endif

template <typename T>
class DLL_API MyBuffer { // 导出类模板
public:
void push(T data);
};

// 在动态库的 .cpp 中(关键步骤)
// 强制编译器为 int 类型实例化并导出符号
template class DLL_API MyBuffer<int>;
template class DLL_API MyBuffer<float>;

函数模板

对于模板的初始化,可以使用typename,class,变量名可以使用T或者其他任何

1
2
3
4
5
>基本格式
>template< 形参列表 >
>类声明
template<typename T>
template<class Ty>

模板的常规类型推导需要注意的就是避免T的二义性,可以通过直接指定T的类型来避免,

1
2
3
4
5
6
7
template<typename T>
T max(const T& a, const T& b) {
return a > b ? a : b;
}

//max(1,1.2) //二义性
max<double>(1,1.2)

除了以上简单的类型推导,模板中还有非常有趣的万能引用和折叠,万能引用即使用T&& 来接收参数,通过折叠能够保持参数的左值右值属性不变,进而实现完美转发
以下为折叠规则,可以看到在万能引用(&&)接收参数时可以保证左右值属性不改变

T& + & → T&
T& + && → T&
T&& + & → T&
T&& + && → T&&
函数模板中比较重要的我认为就是可变参数模板,对于写宏函数的时候实现一些全局可用特性非常关键,具体实现如下。

1
2
3
4
5
6
template<typename...Args>
//args 是函数形参包,Args 是类型形参包
void sum(Args...args){
f(args...)
f(&args...)
}

类模板

类模板目前我用的还是比较少的,一个基本的定义是这样的

1
2
3
4
5
6
7
8
9
10
template<typename T>
class Test{
Test(T v) :t{ v } {}
private:
T t;
};
Test t(1); //t是Test<int>
Test(int) -> Test<std::size_t>;

Test t(1); // t 是 Test<size_t>

以音视频处理来举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
class RingBuffer {
private:
T* buffer;
int head;
int tail;
int size;
public:
RingBuffer(int s) : size(s), head(0), tail(0) {
buffer = new T[size];
}
void push(T value) { /* 循环队列逻辑 */ }
T pop() { /* 循环队列逻辑 */ }
~RingBuffer() { delete[] buffer; }
};

// 使用:
RingBuffer<float> audioBuffer(1024); // 存放浮点音频采样
RingBuffer<AVPacket*> packetQueue(50); // 存放音视频包指针

类模板中的函数模板

1
2
3
4
5
6
7
8
9
10
//第一类,类模板中的成员函数模板
template<typename T>
class Class_Template{
void F(T){}
}
//第二类,普通类中的成员函数模板
class Test{
template<typename...Args>
void f(Args&&...args){}
};

类模板中的变量模板

1
2
3
4
5
6
7
8
//普通变量模板见下
//这里主要说静态变量模板
class Class_template{
template<typename T>
static const T min;//声明,也可以使用inline或者constexpr实现类内定义
}
template<typename T>
const T Class_template::min = {};

变量模板

需要注意,变量模板的实例化是一个全局变量
基本的定义方式如下

1
2
3
4
template<typename T>
constexpr T v{};

v<int>; // 相当于 constexpr int v = 0;

模板全特化

模板全特化形式上很像模板情况下的函数重载,但是实际上是完全不同的,即著名的 C++ 专家 Herb Sutter 曾说过:”Specializations don’t overload.”(特化不参与重载解析)。模板全特化是模板的附属品,编译器选定一个基础模板后才会去检查有没有对应的特化版本,且形参列表必须完全匹配,不能够存在隐式转换。而对于函数重载,每个函数都是一个独立的竞争队形,编译器会在所有的重载函数中选择一个最合适的,其中可以进行隐式转换。

1
2
3
4
5
6
7
8
9
//函数模板
template<typename T,typename T2>
auto f(const T& a, const T2& b) {
return a + b;
}
template<>
auto f(const double& a, const int& b){
return a - b;
}

而对于类的全特化

1
2
3
4
5
6
7
8
9
10
11
12
13
// 通用版本
template <typename T>
class Matrix {
T* data;
// ... 通用矩阵运算
};

// 全特化版本:专门针对 bool 类型进行空间优化
template <>
class Matrix<bool> {
uint8_t* packedData; // 使用位存储,1字节存8个bool
// ... 专门的位运算实现
};

模板偏特化

相较于全特化要求类型完全相等于A然后取得A(类、变量、函数),偏特化在使用的时候不要求template<>,e而是template ,这意味着,这个(类、变量)仍是一个模板。另外需要注意的是,仅仅有变量和类具备偏特化特性,而函数没有,原因是函数可以通过重载来实现着一个特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 基础模板:通用的缓冲区
template <typename T, typename Strategy>
class BufferProcessor {
// 通用的慢速拷贝逻辑
};

// 2. 偏特化:只要 Strategy 是 "FastDMA",无论数据类型 T 是什么,都走这
// 这里保留了 T,意味着它仍然是模板,自由度受限但依然存在
template <typename T>
class BufferProcessor<T, FastDMA> {
// 针对 DMA 的通用硬件加速逻辑
};

// 3. 全特化:针对 Strategy 是 "FastDMA" 且 数据类型 T 恰好是 "AVFrame"
// 自由度完全消失,变为具体的实现
template <>
class BufferProcessor<AVFrame, FastDMA> {
// 针对 FFmpeg 视频帧的极致优化(比如 YUV 分量内存对齐拷贝)
};

折叠表达式

假设我们有一个参数包 args,包含元素 $E_1, E_2, \dots, E_n$,运算符为 $\circ$(比如 +),初始值为 $I$
一元左折叠,(… op args),(((E1 + E2) + E3) … + En),省略号在左,从左往右算
一元右折叠,(args op …),(E1 + (E2 … + (En-1 + En))),省略号在右,从右往左算
二元左折叠,(init op … op args),((((I + E1) + E2) … + En),有初始值,从左侧开始“吞噬”
二元右折叠,(args op … op init),(E1 + (E2 … + (En + I))),有初始值,从右侧开始“合并”

因此通过定义不同的操作符和不同的折叠顺序,能够执行可变参数的运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一元折叠
template <typename... T>
void initComponents(T... args) {
// 一元左折叠 + 逗号运算符
// 展开为:(initA(), initB()), initC();
(... , args.init());
}
//二元折叠
// 二元左折叠:实现一个万能打印
template <typename... Args>
void printAll(Args... args) {
// 初始值是 std::cout,操作符是 <<
(std::cout << ... << args) << std::endl;
}

待决名

在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是>类型,除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)。

所谓“待决”(Dependent),指的是一个名字依赖于模板参数 T。在模板被实例化之前,编译器根本不知道这个名字到底代表什么。

1
2
3
4
template <typename T>
void function() {
T::iterator * ptr; //无法识别iterator
}

因此引入两个关键字来告知编译器这些未被实例化的、无法识别的属性

  1. typename
    用于反应类内部的类型,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
class PacketQueue {
public:
// 类型别名:这也是类内的类型
using ValueType = T;
using Iterator = T*;
struct A{}
void push(ValueType val) { /* ... */ }
};
template <typename T>
void handle(T& obj) {
// 编译器看到 T::Config 时,它不知道这是一个【嵌套结构体名】还是一个【静态成员变量】。
// 如果没有 typename,它默认当成【变量】。
// 如果它是【类内的类型】,你必须显式加 typename。
typename T::Config myConfig;
}
  1. template
    用于反应类内部定义的模板,例如函数模板、变量模板、类模板等等
1
2
3
4
5
6
7
8
9
10
11
struct RegularEncoder {
template <typename T>
void encode(T data) { /* ... */ }
};

template <typename TEncoder>
void startStreaming(TEncoder& enc) {
// 编译器看到这里报错,无法解析encode中的待决名
// enc.encode<AVPacket>(packet);
enc.template encode<AVPakcet>(packet);
}

本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。