泛型编程(Generic Programming)

泛型编程是一种编程范式,旨在编写与类型无关的代码。C++中的模板是实现泛型编程的工具。通过泛型编程,程序员可以编写更通用、更可重用的代码。

特点

  1. 类型无关:通过模板,代码可以适用于多种数据类型,而无需为每种类型编写单独的实现。
  2. 编译时多态:模板通过在编译时生成特定类型的代码,提供了一种编译时的多态性,与运行时多态(如虚函数)不同。
  3. 提高代码重用性:模板使得同一段代码可以用于多个数据类型,从而提高代码的重用性。

模板(Template)

模板是C++中的一种机制,用于创建通用的函数和类。模板允许在编写代码时不指定具体的数据类型,而是在使用时再指定。这使得同一段代码可以处理不同的数据类型。

函数模板

函数模板用于定义可以接受任意类型参数的函数。以下是一个简单的函数模板示例:

1
2
3
4
5
6
7
8
9
10
template <typename T>
T add(T a, T b) {
return a + b;
}

int main() {
std::cout << add(3, 4) << std::endl; // 使用 int
std::cout << add(3.5, 4.5) << std::endl; // 使用 double
return 0;
}

在这个例子中,add 函数是一个函数模板,可以用于不同的数据类型,如 intdouble

类模板

类模板用于定义可以处理任意类型数据的类。以下是一个简单的类模板示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T> //。模板参数前有个typename关键字,这里可以写成typename,也可以写成class(这里的class显然不是用来定义类的),这是固定写法.
class Box {
private:
T value;
public:
Box(T val) : value(val) {}
T getValue() const { return value; }
};

int main() {
Box<int> intBox(123);
Box<double> doubleBox(45.67);

std::cout << intBox.getValue() << std::endl;
std::cout << doubleBox.getValue() << std::endl;

return 0;
}

在这个例子中,Box 是一个类模板,可以用于不同的数据类型。

类型参数和非类型参数

类型参数(Type Parameter)

类型参数用于指定模板中的数据类型。类型参数使得模板能够处理不同的数据类型,而无需为每种类型编写单独的代码。

使用方法

类型参数通常使用 typenameclass 关键字来声明。二者在这个上下文中是等价的。以下是一些例子:

  • 函数模板
1
2
3
4
template <typename T>
T add(T a, T b) {
return a + b;
}

在这个例子中,T 是一个类型参数,表示函数 add 可以接受任何类型的参数,只要这些参数支持 + 操作。

  • 类模板
1
2
3
4
5
6
7
8
template <class T = string> // 这里可以指定默认值
class Box {
private:
T value;
public:
Box(T val) : value(val) {}
T getValue() const { return value; }
};

在这个例子中,T 是一个类型参数,表示 Box 类可以存储任何类型的值。

非类型参数(Non-Type Parameter)

非类型参数用于指定模板中的常量值。这些参数在实例化模板时必须是已知的常量表达式。

使用方法

非类型参数通常用于指定数组大小、常量值等。非类型参数可以是整型、枚举、指针或引用类型。以下是一些例子:

  • 类模板
1
2
3
4
5
6
7
template <typename T, int Size>
class Array {
private:
T data[Size];
public:
int getSize() const { return Size; }
};

在这个例子中,Size 是一个非类型参数,表示数组的大小。Array 类可以用于创建不同大小的数组。

  • 函数模板

    非类型参数在函数模板中较少使用,但可以用于固定某些常量值的计算。

非类型参数的限制

  • 必须是一个常量表达式。
  • 通常是整型、枚举、指针或引用类型。
  • 不能是浮点数或类类型。

typename特殊情况

虽然 typenameclass 在模板参数列表中是等价的,但在其他上下文中,typename 有一个特定的用途:

  • 嵌套依赖类型:当你需要在模板中访问依赖于模板参数的嵌套类型时,必须使用 typename 来告诉编译器该名称是一个类型。例如:
1
2
3
4
template <typename T>
void func() {
typename T::NestedType value; // 使用 typename 指明 NestedType 是一个类型
}

在这个例子中,typename 用于指明 T::NestedType 是一个类型,而不是一个静态成员或其他实体。

模版的实例化

主要作用

  1. 生成具体类型或函数:模板实例化的主要作用是从通用的模板定义生成特定的类或函数。这使得同一段代码可以适用于多种数据类型,而无需重复编写。

  2. 代码重用:通过模板实例化,程序员可以编写一次代码,并在多个不同的上下文中重用。这大大提高了代码的重用性和维护性。

  3. 类型安全:模板实例化在编译时进行类型检查,这意味着任何类型不匹配的问题都会在编译时被捕获,从而提高了程序的安全性和可靠性。

模板实例化的过程

模板实例化可以是显式的,也可以是隐式的。

  • 隐式实例化:当你使用模板时,编译器会自动为你实例化模板。例如:
1
2
3
4
5
6
7
8
9
template <typename T>
T add(T a, T b) {
return a + b;
}

int main() {
int result = add(3, 4); // 隐式实例化 add<int>
return 0;
}

在这个例子中,当 add(3, 4) 被调用时,编译器会自动实例化 add<int>

  • 显式实例化:有时你可能希望提前实例化模板,以便在某些情况下减少编译时间或避免代码膨胀。可以通过显式实例化来实现:
1
template int add<int>(int, int); // 显式实例化,只在cpp文件里定义一次

这行代码告诉编译器为 int 类型显式实例化 add 函数模板。

其他cpp文件引用:

1
extern template template int add<int>(int, int);

using 和typedef的区别

基本用法

  • typedef:用于为已有类型定义一个新的名称。
1
typedef unsigned long ulong;

在这个例子中,ulongunsigned long 的别名。

  • using:在C++11中引入,提供与typedef相同的基本功能。
1
using ulong = unsigned long;

这行代码与上面的typedef示例等效。

主要区别

  1. 语法和可读性

    • using 的语法更接近于变量和函数的声明方式,通常被认为更易读。
    • typedef 的语法在处理复杂类型(如指针、函数指针等)时可能显得不够直观。
  2. 模板别名

    • using 支持模板别名,这是typedef无法实现的。
1
2
3
4
template <typename T>
using Vec = std::vector<T>;

Vec<int> myVector; // 等价于 std::vector<int>

在这个例子中,Vecstd::vector 的模板别名,可以用于任何类型 T

  1. 作用域
    • using 可以用于引入命名空间中的名称到当前作用域。
1
2
3
4
5
6
7
namespace A {
class MyClass {};
}

using A::MyClass;

MyClass obj; // 直接使用 MyClass 而不需要 A::

typedef 不具备这种功能。

  1. 类型转换
    • 在类型转换的上下文中,usingtypedef 的行为是相同的,因为它们都只是类型的别名,不会影响实际的类型转换规则。

何时使用 usingtypedef

  • 简单类型别名:在为简单类型定义别名时,usingtypedef 都可以使用,但using通常更具可读性。

  • 模板别名:当需要为模板创建别名时,必须使用using,因为typedef不支持模板别名。

  • 复杂类型:在处理复杂类型(如函数指针、复杂模板类型等)时,using通常比typedef更易于理解和使用。

全特化和偏特化

全特化(Full Specialization)

全特化是指为某个具体类型提供模板的完整实现。在全特化中,所有模板参数都被具体化,即你为模板指定了特定的类型或值。

使用场景

全特化通常用于为某些特定类型提供特殊的实现,比如优化性能或处理特定类型的特殊逻辑。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

// 通用模板
template <typename T>
class MyClass {
public:
void print() {
std::cout << "Generic template" << std::endl;
}
};

// 全特化版本
template <>
class MyClass<int> {
public:
void print() {
std::cout << "Specialized for int" << std::endl;
}
};

int main() {
MyClass<double> obj1;
obj1.print(); // 输出: Generic template

MyClass<int> obj2;
obj2.print(); // 输出: Specialized for int

return 0;
}

在这个例子中,MyClass<int> 是对 MyClass 的全特化,为类型 int 提供了一个特定的实现。

偏特化(Partial Specialization)

偏特化是指为某些模板参数提供部分具体化,而其他参数仍然保持通用性。偏特化通常用于类模板,因为C++不支持函数模板的偏特化。

使用场景

偏特化用于处理模板参数的某些组合情况,而不需要为所有可能的参数组合提供特化版本。它在处理复杂类型组合和条件时非常有用。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

// 通用模板
template <typename T, typename U>
class MyPair {
public:
void print() {
std::cout << "Generic template" << std::endl;
}
};

// 偏特化版本:当两个类型相同时 U=T
template <typename T>
class MyPair<T, T> {
public:
void print() {
std::cout << "Partial specialization for same types" << std::endl;
}
};
// 偏特化版本:当 U类型等于int
template <typename T>
class MyPair<T, int> {
public:
void print() {
std::cout << "Partial specialization for same types" << std::endl;
}
};

int main() {
MyPair<int, double> obj1;
obj1.print(); // 输出: Generic template

MyPair<int, int> obj2;
obj2.print(); // 输出: Partial specialization for same types

return 0;
}

在这个例子中,MyPair<T, T> ,MyPair<T, int>是对 MyPair<T, U> 的偏特化,处理当两个模板参数类型相同时的情况。

总结

  • 全特化:为特定的类型或值提供完全实现。适用于需要对某个特定类型进行特殊处理的情况。
  • 偏特化:为部分模板参数提供具体化,而其他参数保持通用。适用于需要处理某些参数组合或条件的情况。

模版模版参数

模板模板参数(Template Template Parameters)是C++的一项高级特性,允许模板参数本身也是模板。这种功能使得编写更通用和灵活的代码成为可能,特别是在设计需要处理多个模板类型的库或类时。

基本概念

模板模板参数允许你定义一个模板,其参数是另一个模板。例如,你可以编写一个模板类,它的一个参数是一个容器模板(如std::vectorstd::list),而不关心容器中存储的具体类型。

语法

模板模板参数的语法稍微复杂一些,因为你需要指定模板参数本身是一个模板。以下是一个简单的语法结构:

1
2
3
4
5
6
7
8
template <template <typename> class Container>
class MyClass {
Container<int> data;
public:
void add(int element) {
data.push_back(element);
}
};

在这个例子中,Container是一个模板模板参数,表示一个接受单个类型参数的模板类。

示例

以下是一个完整的示例,展示如何使用模板模板参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <vector>
#include <list>

template <template <typename, typename...> class Container>
class MyClass {
Container<int> data;
public:
void add(int element) {
data.push_back(element);
}

void print() const {
for (const auto& elem : data) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};

int main() {
MyClass<std::vector> vecObj;
vecObj.add(1);
vecObj.add(2);
vecObj.add(3);
vecObj.print(); // 输出: 1 2 3

MyClass<std::list> listObj;
listObj.add(4);
listObj.add(5);
listObj.add(6);
listObj.print(); // 输出: 4 5 6

return 0;
}

在这个例子中,MyClass是一个接受模板模板参数的类模板。它可以与任何符合标准库容器接口的模板类一起使用,如std::vectorstd::list

注意事项

  1. 参数数量:模板模板参数的模板参数数量必须匹配。例如,如果你定义了一个模板模板参数要求一个模板有两个参数,那么传入的模板必须有两个参数。

  2. 可变参数模板:为了兼容更多的标准库容器(如std::vector,其构造函数接受可变数量的模板参数),通常会使用可变参数模板(typename...)来定义模板模板参数。

  3. 复杂性:模板模板参数增加了模板的复杂性,可能会导致更难以理解和调试的代码。因此,应在需要时谨慎使用。
    可变参数模板(Variadic Templates)是C++11引入的一项功能,允许模板接受可变数量的模板参数。这为编写更通用和灵活的代码提供了强大的支持,特别是在需要处理不定数量的参数时。

可变参数模版

可变参数模板可以用于函数模板和类模板,允许它们接受任意数量的模板参数。它们通过使用模板参数包(parameter pack)来实现,这是一种可以包含零个或多个模板参数的特殊类型。

语法

可变参数模板使用省略号(...)来表示参数包。以下是基本的语法结构:

1
2
3
4
template <typename... Args>
void func(Args... args) {
// 处理参数包
}

在这个例子中,Args... 是一个模板参数包,args... 是一个函数参数包。Argsargs 可以包含任意数量的参数。

示例

以下是一些使用可变参数模板的示例:

函数模板

一个简单的示例是实现一个可以接受任意数量参数的打印函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

template <typename T>
void print(T value) {
std::cout << value << std::endl;
}

template <typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // 递归调用
}

int main() {
print(1, 2.5, "Hello", 'A');
return 0;
}

在这个例子中,print 函数使用递归来处理参数包,直到所有参数都被打印。

类模板

可变参数模板也可以用于类模板,例如实现一个简单的元组类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

template <typename... Args>
class MyTuple;

template <>
class MyTuple<> {
public:
void print() const {}
};

template <typename T, typename... Args>
class MyTuple<T, Args...> : private MyTuple<Args...> {
T value;
public:
MyTuple(T v, Args... args) : MyTuple<Args...>(args...), value(v) {}

void print() const {
std::cout << value << " ";
MyTuple<Args...>::print();
}
};

int main() {
MyTuple<int, double, std::string> myTuple(1, 2.5, "Hello");
myTuple.print(); // 输出: 1 2.5 Hello
return 0;
}

在这个例子中,MyTuple 类模板通过继承来递归处理参数包,实现了一个简单的元组。

注意事项

  1. 递归展开:处理参数包时,通常使用递归展开的方法,即通过递归调用来逐个处理参数。

  2. 基准案例:递归展开时需要定义一个基准案例(如上例中的单参数 print 和空 MyTuple 特化),以终止递归。

  3. 编译器支持:可变参数模板是C++11标准的一部分,因此需要确保编译器支持这一特性。