c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译

简介: 在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;


前文已经介绍了模板的初阶,介绍了函数模板与类模板,那么这篇文章就针对模板在更近一步,介绍模板进阶内容:非类型模板参数、模板的特化以及模板的分离编译。

非类型模板参数
模板参数可分为类型形参和非类型形参。
类型模板参数:在模板参数列表中 ,是class或typename关键字之后的参数类型名称,也就是我们在初阶文章所用的那类表示。

比如:

template//类型形参
class 类模板名
{
private:
  //类内成员声明
};

非类型模板参数: 用一个常数作为类(函数)模板中的一个参数,在类(函数)模板中可将该参数当成常量来使用。

比如:

template
class MyClass {
public:
static const T value = value;
};

这里 T 是类型模板参数,而 value 是非类型模板参数。

当然还可以用其value创建一个静态的数组:

template
class MyClass {
public:
static const T value = value;
private:
T _arr[value];
};
这样设计的代码就可以通过利用非类型模板参数进行定义静态数组。

int main()
{
MyClass s1;//创建了一个大小为存储10个int类型的静态数组
cout << sizeof(s1) << endl;//打印结果:40
MyClass s2;//创建了一个大小为存储10个int类型的静态数组
cout << sizeof(s2) << endl;//打印结果:400
return 0;
}
注意:

    1类型限制:非类型模板参数只允许使用整型家族(整型类型,字符类型)类对象以及字符串                              是不允许作为非类型模板参数的。

    2作用域:非类型模板参数的作用域是模板定义的整个范围。

    3类型推断:在使用非类型模板参数时,编译器会根据提供的值推断出参数的类型。

非类型模板参数的优点:

控制固定大小的数组或容器。
根据编译时常量调整算法的行为。
提高性能,通过直接嵌入常量值避免运行时开销。
模板的特化
概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行大于比较的函数模板

template
bool Less(T left, T right)
{
return left > right;
}

在我们进行简单的数字比较时

int main()
{
cout << Less(1, 2) << endl; // 可以比较,结果正确
return 0;
}

这样使用是没有问题的,它的判断结果也是我们所预期的,但是我们也可能会这样去使用该函数模板:

int main()
{
int a = 10;
int b = 5;
int p1 = &a, p2 = &b;
cout << Less(p1, p2) << endl; // 不可以比较,结果错误
//实际答案是false,期望结果为true
return 0;
}

判断结果是这两个指针是否构成大于关系,这很好理解,因为我们希望的是该函数能够判断两个指针所指向的内容是否构成大于关系,而该函数实际上判断是确实这两个指针所存储的地址是否构成大于关系,这是两个存在于栈区的指针,所指向的内容不同,其所存储的地址显然是不同的。
类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方

函数模板特化
依据上面给出的案例,我们得知,当传入的是指针时,我们所期望的是进行比较两者指向的空间存储的信息,而不是比较其二者指针的存储信息,那么此时我们就可以对指针类型进行特殊化的实现,从而达到我们所期望的效果。

//基础的函数模板
template
bool Less(T left, T right)
{
return left > right;
}

//对于指针类型的特化
template
bool Less(T left, T right)
{
return left > right;
}

//对于int类型的特化
bool Less(int
left, int right)
{
return
left > *right;
}

类模板特化
不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化和偏特化(半特化)。

在介绍类模板的特化之前先介绍一下仿函数

仿函数介绍

include

using namespace std;
class Add {
public:
// 重载函数调用运算符
int operator()(int x, int y) const {
return x + y;
}
};
int main() {
Add add; // 创建 Add 类的对象
cout << "3 + 4 = " << add(3, 4) << endl; // 使用仿函数
return 0;
}

仿函数的优势
状态管理:仿函数可以包含数据成员,因此可以在函数调用之间保持状态。例如,可以创建一个仿函数来计算运行和:
class Accumulate {
private:
int sum;
public:
Accumulate() : sum(0) {}

void operator()(int value) {  
    sum += value;  
}  

int getSum() const {  
    return sum;  
}  

};

可作为模板参数:仿函数可以作为模板参数传递给 STL 算法,提高代码的灵活性和可重用性。
为什么会在这里介绍仿函数呢?

这是因为仿函数与类模板有着密切的关系:

仿函数与类模板的关系
仿函数可以是类模板:我们可以创建一个类模板来定义仿函数,以便它可以处理不同的数据类型。例如:
template
class Add {
public:
T operator()(T x, T y) const {
return x + y;
}
};

include

include

include

using namespace std;

template
class Compare {
public:
bool operator()(T a, T b) const {
return a > b; // 默认降序
}
};

int main() {
vector vec = {5, 3, 8, 1, 2};
// 使用 Compare 仿函数模板对 vec 进行排序
sort(vec.begin(), vec.end(), Compare());
for (int n : vec)
{
cout << n << " ";
} // 输出: 8 5 3 2 1
return 0;
}

相比之下,类模板其实就是在仿函数的基础上进行修改,使得达到我们想要达到的要求,就比如说全特化:

全特化

举例代码:

//普通的类模板
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};

int main()
{
Dd1;//打印:cout << "D" << endl;
Dd2;//打印:cout << "D" << endl;
return 0;
}

当T1和T2是int,int时,我们若是想对实例化的类进行特殊化处理,那么我们就可以对T1和T2是int和int时的模板进行特化。

首先必须要有一个基础的类模板。
关键字template后面接一对空的尖括号<>。
类名后跟一对尖括号,尖括号中指定需要特化的类型。
生成就会生成一个一个全特化的类模板函数

那么如何证明当T1是int,T2是int时,使用的就是我们自己特化的类模板呢?
当我们实例化一个对象时,编译器会自动调用其默认构造函数,我们若是在构造函数当中打印适当的提示信息,那么当我们实例化对象后,通过观察控制台上打印的结果,即可确定实例化该对象时调用的是不是我们自己特化的类模板了

打印结果:

int main()
{
Dd1;
Dd2;
return 0;
}

偏特化
全特化理解后,偏特化就很好理解了,全特化就是将全部特化,那偏特化不就是特化部分嘛其定义:

//偏特化的类模板
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
char b;
};
但是如果这样定义类,那么他走什么呢?

int main()
{
Dd1;
Dd2;
return 0;
}

实际上是:

他的匹配原则就是就近匹配,谁最匹配,就走谁。

补充:
偏特化并不仅仅是指特化部分参数,而是针对模板参数进一步的条件限制所设计出来的一个特化版本。
例如,我们还可以指定当T1和T2为某种类型时,使用我们特殊化的类模板。

//两个参数偏特化为指针类型
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};
//两个参数偏特化为引用类型
template
class D
{
public:
D()
{
cout << "D" << endl;
}
private:
T1 a;
T2 b;
};
int main()
{
Dd1;
Dd2;
Dd3;
return 0;
}

运行结果:

模板的分离编译
什么是分离编译

在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。

按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:

但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错。

下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:

预处理: 头文件展开、去注释、宏替换、条件编译等。生成预处理后的代码(main.i)
编译: 检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。将源代码(main.i)转换为汇编(main.s),此时并未具体化 Add 函数模板。
汇编: 把编译阶段生成的文件转成目标文件。将汇编代码转换为目标文件(main.o)。
链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作。

经过预处理后,就剩下两个文件了一个是Add.i,一个是test.i ,

这两个文件内容如下:

经过预处理后,接下来进入编译阶段。虽然在 main.i 中调用了 Add 函数,但由于 main.i 同时也包含了 Add 函数模板的声明,因此在编译阶段不会发现任何语法错误。编译器顺利将 Add.i 和 main.i 翻译成了汇编语言,并在 Linux 操作系统中生成了 Add.s 和 main.s 文件。

接下来是汇编阶段,编译器利用 Add.s 和 main.s 分别生成了两个目标文件,即 Add.o 和 main.o,这些文件为后续的链接阶段做准备。

到此为止,预处理、编译和汇编过程都顺利完成。但在进行链接操作时,出现了问题:main 函数中调用的 Add 函数并没有得到正确链接。问题的根源在于,Add 函数模板并没有生成实际的函数定义。之所以如此,是因为在整个过程中,函数模板的模板参数 T 并未被实例化,导致编译器无法确定 T 应该对应何种类型。因此,Add 函数模板没有生成具体的函数实现,链接时也就无法找到相应的符号。

模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(test.cpp)没有模板函数的定义,无法进行实例化。

对应的修改方法就是将Add.c文件修改就可以

//Add.c
//函数模板的定义
template
T Add(const T& x, const T& y)
{
return x + y;
}
template
T Add(const int& x, const int& y);

总结:

带来的优点

减少了代码的重复,从而提高了维护性,
模板在编译时进行类型检查,确保类型安全,避免了类型转换引发的错误。
由于模板是在编译期间实例化的,生成的代码经过优化,通常比运行时多态(如虚函数)更高效。
模板可以用于函数和类,能够处理多种数据类型,提供了高度的灵活性
模板是实现泛型编程的基础,允许算法和数据结构与类型分离,增强了代码的通用性。
缺点:

模板不支持某些类型(如浮点数、类对象、不为非类型参数提供完整类型限制等),可能导致设计上的限制。
对于每种类型的模板实例,编译器会生成独立的代码,这可能导致代码大小增加,尤其是在使用模板的情况下
使用模板可能导致编译时间显著增加,因为模板实例化和类型检查需要消耗额外的时间。

目录
相关文章
|
6月前
|
自然语言处理 编译器 C语言
为什么C/C++编译腰要先完成汇编
C/C++ 编译过程中先生成汇编语言是历史、技术和实践的共同选择。历史上,汇编语言作为成熟的中间表示方式,简化了工具链;技术上,分阶段编译更高效,汇编便于调试和移植;实践中,保留汇编阶段降低了复杂度,增强了可移植性和优化能力。即使在现代编译器中,汇编仍作为重要桥梁,帮助开发者更好地理解和优化代码。
98 25
为什么C/C++编译腰要先完成汇编
|
8月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
652 56
|
8月前
|
自然语言处理 编译器 Linux
|
9月前
|
Linux 编译器 C语言
Linux c/c++之多文档编译
这篇文章介绍了在Linux操作系统下使用gcc编译器进行C/C++多文件编译的方法和步骤。
118 0
Linux c/c++之多文档编译
|
5月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
47 0
|
1月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
117 0
|
3月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
115 12
|
4月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
101 16
|
4月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。