总结C++的模板template编程技术

总结C++的模板template编程技术

C++中的模板(template)是一种编程技术,它允许您在编写代码时定义通用的类或函数,而不需要指定具体的数据类型。在实例化模板时,编译器会根据提供的类型自动生成相应的代码。模板可以提高代码重用性和可维护性,并减少重复代码。

模板可以分为两类:函数模板和类模板。

函数模板

函数模板是用于创建通用函数的模板。它们可以用于多种数据类型,而无需为每种类型编写单独的函数。函数模板的语法如下:

1
2
3
4
5
template <typename T>
return_type function_name(parameters) {
// function body
}


例如,下面是一个通用的比较函数,可用于比较任何数据类型的两个值:
1
2
3
4
5
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}


使用函数模板的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

int main() {
int a = 3, b = 5;
std::cout << "Max of " << a << " and " << b << " is " << max(a, b) << std::endl;

double x = 3.5, y = 2.5;
std::cout << "Max of " << x << " and " << y << " is " << max(x, y) << std::endl;

char c1 = 'A', c2 = 'B';
std::cout << "Max of " << c1 << " and " << c2 << " is " << max(c1, c2) << std::endl;

return 0;
}

类模板(Class Templates):

类模板用于创建通用类,可以用于多种数据类型。类模板的语法如下:

1
2
3
4
5
template <typename T>
class ClassName {
// class members
};


例如,下面是一个简单的通用堆栈类:
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
template <typename T>
class Stack {
private:
T arr[10];
int top;

public:
Stack() : top(-1) {}

void push(T value) {
if (top >= 9) {
std::cout << "Stack overflow" << std::endl;
return;
}
arr[++top] = value;
}

T pop() {
if (top < 0) {
std::cout << "Stack underflow" << std::endl;
return T();
}
return arr[top--];
}

bool isEmpty() const {
return top == -1;
}
};


使用类模板的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "Stack.h"

int main() {
Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);

while (!intStack.isEmpty()) {
std::cout << "Popped: " << intStack.pop() << std::endl;
}

Stack<std::string> stringStack;
stringStack.push("Hello");
stringStack.push("World");
stringStack.push("!");
while (!stringStack.isEmpty()) {
std::cout << "Popped: " << stringStack.pop() << std::endl;
}

return 0;
}

在这个例子中,我们创建了两个不同类型的堆栈:一个用于整数,另一个用于字符串。Stack类的实例化将根据所需的类型生成相应的代码。注意,在使用类模板时,您需要在类名后面使用尖括号<>来指定实际类型。

总结:C++模板是一种强大的编程技术,可以帮助您编写更通用、可重用的代码。函数模板和类模板是两种主要的模板类型,它们都允许您为多种数据类型创建代码,而无需为每种类型编写单独的实现。

非类型模板参数(Non-type Template Parameters):

除了允许传递类型参数之外,模板还支持传递非类型参数。这些参数可以是整数、枚举、指向对象的指针或引用等。例如,我们可以创建一个用于存储固定大小数组的类模板:

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
template <typename T, int SIZE>
class FixedSizeArray {
private:
T arr[SIZE];
int count;

public:
FixedSizeArray() : count(0) {}

void add(T value) {
if (count >= SIZE) {
std::cout << "Array is full" << std::endl;
return;
}
arr[count++] = value;
}

T get(int index) const {
if (index < 0 || index >= count) {
std::cout << "Index out of bounds" << std::endl;
return T();
}
return arr[index];
}
};


使用非类型模板参数的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include "FixedSizeArray.h"

int main() {
FixedSizeArray<int, 3> intArray;
intArray.add(1);
intArray.add(2);
intArray.add(3);
intArray.add(4); // This will output "Array is full"

std::cout << "Element at index 1: " << intArray.get(1) << std::endl;

return 0;
}

模板特化(Template Specialization):

模板特化允许您为模板提供特定类型或非类型参数的特殊实现。特化可以是全特化(所有参数都明确指定)或部分特化(只指定部分参数)。

全特化示例:

1
2
3
4
5
6
7
8
template <>
class Stack<bool> {
// Specialized implementation for bool type
};

// Usage:
Stack<bool> boolStack;

部分特化示例:

1
2
3
4
5
6
7
8
template <typename T>
class Stack<T*> {
// Specialized implementation for pointer types
};

// Usage:
Stack<int*> intPtrStack;

变长模板参数(Variadic Template Parameters):

变长模板参数允许您在模板中使用可变数量的类型参数。可以通过在模板参数列表中使用省略号(…)来表示变长模板参数。这可以用于创建递归模板,例如实现类型安全的元组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename... Ts>
struct Tuple;

template <typename Head, typename... Tail>
struct Tuple<Head, Tail...> {
Head head;
Tuple<Tail...> tail;
};

template <>
struct Tuple<> {}; // Empty tuple specialization

// Usage:
Tuple<int, double, std::string> myTuple;

模板元编程(Template Metaprogramming):

模板元编程是一种在编译时执行计算的技术,它主要使用C++模板来实现。模板元编程可以用于生成高度优化的代码、在编译时进行类型检查等。一个经典的模板元编程示例是计算阶乘:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <unsigned int N>
struct Factorial {
enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0> {
enum { value = 1 };
};

// Usage:
constexpr unsigned int result = Factorial<5>::value; // result = 120

模板别名(Template Aliases):

C++11引入了模板别名,允许您为模板定义一个简短的别名,以提高代码可读性和简化模板使用。例如,我们可以为std::vector和std::map定义别名:

1
2
3
4
5
6
7
8
9
10
template <typename T>
using Vec = std::vector<T>;

template <typename Key, typename Value>
using Map = std::map<Key, Value>;

// Usage:
Vec<int> intVector;
Map<std::string, int> stringToIntMap;

类型萃取(Type Traits):

C++标准库提供了一组模板类,称为类型萃取(type traits),它们可以在编译时提供关于类型的信息,例如是否是整数类型、是否是指针类型等。类型萃取可以用于编写更通用和自适应的代码。例如,我们可以使用std::enable_if和std::is_integral来创建一个仅接受整数类型参数的函数模板:

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

template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
void printIfIntegral(T value) {
std::cout << "Value: " << value << std::endl;
}

int main() {
printIfIntegral(42); // OK
// printIfIntegral(3.14); // Error: This line will not compile
return 0;
}

SFINAE (Substitution Failure Is Not An Error):

SFINAE是C++模板编程中的一种技术,它允许编译器在模板参数替换失败时回退到其他可用的模板。这种特性在泛型编程中尤为有用,因为它使得模板可以更加通用且容错。以下示例展示了一个使用SFINAE的简单场景:

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

template <typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type
sum(T a, T b) {
return a + b;
}

template <typename T>
typename std::enable_if<!std::is_arithmetic<T>::value, std::string>::type
sum(T a, T b) {
return "Not an arithmetic type";
}

int main() {
std::cout << sum(1, 2) << std::endl; // Output: 3
std::cout << sum("hello", "world") << std::endl; // Output: Not an arithmetic type
return 0;
}


在这个例子中,我们定义了两个sum函数模板,一个用于算术类型,另一个用于其他类型。std::enable_if和类型萃取std::is_arithmetic用于控制选择合适的函数模板。

C++17中的if constexpr:

C++17引入了if constexpr语句,它允许在编译时进行条件编译。这可以简化某些模板编程场景,使代码更易于阅读和理解。以下是一个使用if constexpr的示例:

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

template <typename T>
void printType(T value) {
if constexpr (std::is_integral<T>::value) {
std::cout << "Integral type: " << value << std::endl;
} else if constexpr (std::is_floating_point<T>::value) {
std::cout << "Floating point type: " << value << std::endl;
} else {
std::cout << "Other type: " << value << std::endl;
}
}

int main() {
printType(42); // Output: Integral type: 42
printType(3.14); // Output: Floating point type: 3.14
printType("hello"); // Output: Other type: hello
return 0;
}


在这个例子中,我们使用if constexpr根据类型特征执行不同的代码。在编译时,仅生成与当前类型匹配的分支的代码,从而提高了代码效率。

这些高级用法和技巧为您提供了深入了解C++模板的机会,并展示了如何利用模板编写更灵活、高效和可维护的代码。不过,需要注意的是,在使用这些高级技巧时,请确保不要过度使用,以免降低代码的可读性。在实际项目中,务必在可读性和性能之间取得平衡。

模板参数推导(Template Argument Deduction):

模板参数推导是编译器在调用函数模板时自动推导模板参数类型的过程。当调用函数模板时,如果没有显式指定模板参数,编译器会根据实参类型自动推导模板参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
void print(T value) {
std::cout << "Value: " << value << std::endl;
}

int main() {
int i = 42;
double d = 3.14;
std::string s = "hello";

print(i); // T is deduced as int
print(d); // T is deduced as double
print(s); // T is deduced as std::string
return 0;
}

C++14中的泛型Lambda表达式:

C++14引入了泛型Lambda表达式,使得Lambda函数能够接受模板参数。这进一步简化了对泛型代码的编写。例如:

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

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int factor = 2;

// Generic Lambda expression
auto multiply = [factor](auto x) { return x * factor; };

std::transform(numbers.begin(), numbers.end(), numbers.begin(), multiply);

for (const auto& number : numbers) {
std::cout << number << " ";
}
// Output: 2 4 6 8 10
return 0;
}


在这个示例中,我们定义了一个泛型Lambda表达式multiply,它接受一个泛型参数x,然后返回x与factor相乘的结果。

C++20中的概念(Concepts):

C++20引入了概念(Concepts),这是一种指定模板参数约束的新方法。概念使得模板参数的约束变得更加明确,同时提供了更好的编译时错误信息。以下是一个使用概念的示例:

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

template <typename T>
concept Number = std::is_arithmetic_v<T>;

template <Number T>
T add(T a, T b) {
return a + b;
}

int main() {
std::cout << add(1, 2) << std::endl; // OK
// std::cout << add("hello", "world") << std::endl; // Error: Constraints not satisfied
return 0;
}


在这个例子中,我们定义了一个名为Number的概念,它要求模板参数T是一个算术类型。接下来,我们定义了一个使用该概念的模板函数add。当我们尝试使用不满足约束的类型调用add函数时,编译器将报错并提供有关未满足概念约束的详细信息。

C++20中的requires表达式:

requires表达式是C++20中另一个与概念相关的特性,它可以用于检查类型是否满足一组特定的约束。requires表达式可以与概念一起使用,也可以单独使用。以下是一个使用requires表达式的示例:

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

template <typename T>
requires std::is_arithmetic_v<T>
T multiply(T a, T b) {
return a * b;
}

int main() {
std::cout << multiply(3, 4) << std::endl; // OK
// std::cout << multiply("hello", "world") << std::endl; // Error: Constraints not satisfied
return 0;
}


在这个例子中,我们定义了一个模板函数multiply,它要求模板参数T是一个算术类型。我们使用requires表达式来指定这个约束。与概念一样,当我们尝试使用不满足约束的类型调用multiply函数时,编译器将报错。

C++20中的模板参数列表:

C++20引入了模板参数列表(template parameter lists),它允许在一个模板中定义多个参数列表。这使得模板更加灵活,能够接受更多种类的参数。以下是一个使用模板参数列表的示例:

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

template <typename T>
struct MyStruct {
T value;

template <typename U>
void foo(U arg) {
std::cout << "T: " << value << ", U: " << arg << std::endl;
}
};

int main() {
MyStruct<int> s1{42};
s1.foo("hello"); // Output: T: 42, U: hello

MyStruct<std::string> s2{"world"};
s2.foo(3.14); // Output: T: world, U: 3.14

return 0;
}


在这个例子中,我们定义了一个MyStruct模板类,它有一个类型参数T和一个函数模板参数列表。这使得我们可以在一个模板中同时使用不同类型的参数。

C++20中的约束模板参数(constrained template parameters):

C++20中的约束模板参数是一种用于在模板中定义满足一组特定约束的类型参数的方法。以下是一个使用约束模板参数的示例:

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

template <typename T>
concept Printable = requires(T x) {
std::cout << x;
};

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

int main() {
print("hello"); // Output: hello
// print(42); // Error: Type does not satisfy constraints
return 0;
}


在这个例子中,我们定义了一个名为Printable的概念,它要求类型T可以用于输出流。接下来,我们定义了一个使用约束模板参数的模板函数print,它要求模板参数T满足Printable概念。当我们尝试使用不满足约束的类型调用print函数时,编译器将报错。

模板的特化和偏特化:

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>
#include <string>

template <typename T>
struct MyStruct {
void foo() {
std::cout << "Generic implementation" << std::endl;
}
};

template <>
struct MyStruct<int> {
void foo() {
std::cout << "Specialized implementation for int" << std::endl;
}
};

template <typename T, typename U>
struct MyStruct<std::pair<T, U>> {
void foo() {
std::cout << "Partial specialization for std::pair" << std::endl;
}
};

int main() {
MyStruct<double> s1;
s1.foo(); // Output: Generic implementation

MyStruct<int> s2;
s2.foo(); // Output: Specialized implementation for int

MyStruct<std::pair<int, std::string>> s3;
s3.foo(); // Output: Partial specialization for std::pair

return 0;
}


在这个例子中,我们定义了一个通用的模板MyStruct,它有一个函数foo()。接下来,我们为MyStruct提供了一个特化的实现,它有自己的foo()函数实现。最后,我们为MyStruct>提供了一个偏特化的实现,它也有自己的foo()函数实现。

C++20中的模板构造函数(template constructor):

C++20引入了模板构造函数,这是一种使用模板参数的类构造函数。模板构造函数使得我们可以在创建类的实例时指定模板参数,这进一步增加了类的灵活性。以下是一个使用模板构造函数的示例:

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

template <typename T>
struct MyStruct {
T value;

template <typename U>
MyStruct(U arg) : value(arg) {}

void print() {
std::cout << "Value: " << value << std::endl;
}
};

int main() {
MyStruct<int> s1{42};
s1.print(); // Output: Value: 42

MyStruct<std::string> s2{"hello"};
s2.print(); // Output: Value: hello

return 0;
}


在这个例子中,我们定义了一个MyStruct模板类,它有一个构造函数,接受任意类型的参数。在构造函数中,我们使用模板参数推导来推断value成员的类型。这使得我们可以在创建MyStruct实例时指定不同的类型参数。

模板元编程(Template metaprogramming):

模板元编程是一种利用编译时计算来生成代码的技术。它可以在编译时完成某些运算,生成比运行时更高效的代码。模板元编程通常涉及到模板特化、模板递归、模板别名等高级模板技巧。以下是一个简单的模板元编程示例:

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

template <int N>
struct Factorial {
static constexpr int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
static constexpr int value = 1;
};

int main() {
constexpr int result = Factorial<5>::value;
std::cout << "5! = " << result << std::endl; // Output: 5! = 120

return 0;
}


在这个例子中,我们使用模板递归来计算阶乘。我们定义了一个模板Factorial,它有一个静态成员变量value,代表模板参数N的阶乘。当模板参数N大于0时,我们使用模板递归来计算Factorial。当模板参数N等于0时,我们使用特化的实现来计算Factorial<0>。

模板元编程框架(Template metaprogramming framework):

为了方便使用模板元编程,C++社区开发了许多模板元编程框架,例如Boost MPL、Boost Hana、Brigand等。这些框架提供了许多高级模板技巧的实现,例如类型列表、条件分支、类型转换等,可以使模板元编程更加方便和易于理解。以下是一个使用Boost Hana框架的示例:

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

using namespace boost::hana;

template <typename ...T>
void print_types() {
std::cout << "Types: ";
for_each(type_c<T>..., [](auto t) { std::cout << typeid(t).name() << " "; });
std::cout << std::endl;
}

int main() {
print_types<int, std::string, double>(); // Output: Types: iSd
return 0;
}


在这个例子中,我们使用Boost Hana框架提供的type_c和for_each函数来打印类型列表。type_c是一个编译时表示类型T的对象。在print_types函数中,我们使用for_each函数遍历类型列表,并使用typeid函数来打印每个类型的名称。

模板的编译时性能:

尽管C++模板非常强大和灵活,但是它们的编译时性能可能会受到一些限制。在编写模板代码时,我们需要注意一些常见的编译时性能问题,例如:

  • 避免使用递归模板:递归模板可能导致编译器栈溢出或者长时间的编译时间。在使用模板递归时,应该尽量减少递归深度,或者使用尾递归优化等技巧。

  • 尽量避免过度实例化:过度实例化可能导致编译器生成大量的重复代码,增加编译时间和二进制文件大小。在使用模板时,应该尽量减少模板实例化的数量,或者使用模板特化、模板元编程等技巧来减少实例化的复杂度。

  • 尽量避免使用SFINAE和模板嵌套:SFINAE和模板嵌套可能导致编译器生成大量的无用代码,增加编译时间和二进制文件大小。在使用SFINAE和模板嵌套时,应该尽量减少它们的使用频率,或者使用C++20中的概念、requires表达式等技巧来简化代码。

  • 使用模板库:C++社区中有许多优秀的模板库,例如Boost、STL等。这些模板库经过了严格的测试和优化,可以提供高性能和稳定性的模板实现。在使用模板时,应该尽量使用这些模板库,而不是自己实现一些复杂的模板。

总的来说,C++模板是一种非常强大和灵活的编程技术,可以使代码更加通用、高效和易于维护。在编写模板代码时,我们需要注意一些常见的编译时性能问题,以确保代码的可维护性和稳定性。

模板的可读性和可维护性:

尽管C++模板非常强大和灵活,但是它们的语法和使用方法可能会使代码变得难以理解和维护。在编写模板代码时,我们应该注重代码的可读性和可维护性,以确保代码的质量和稳定性。以下是一些提高代码可读性和可维护性的技巧:

  • 使用良好的命名:在定义模板参数、函数模板参数列表、模板类、模板函数等时,应该使用良好的命名,以便于理解和使用。命名应该清晰、简洁和具有描述性,同时遵循C++命名约定。

  • 提供文档和示例:在编写模板代码时,应该提供文档和示例,以帮助其他开发者理解和使用代码。文档应该清晰、详细和易于理解,示例应该涵盖常见的使用场景,并提供正确和简洁的代码实现。

  • 封装复杂的模板:当编写复杂的模板时,应该尽可能将其封装为独立的模块,以便于管理和使用。模块应该提供良好的接口和文档,同时遵循良好的软件设计原则。

  • 使用模板库和模板元编程框架:在编写模板代码时,应该尽可能使用C++社区中已有的模板库和模板元编程框架,以减少重复的代码实现和提高代码质量。这些库和框架经过了严格的测试和优化,可以提供高性能和稳定性的模板实现。

  • 使用概念和requires表达式:C++20引入了概念和requires表达式,它们可以使模板代码更加清晰、简洁和易于理解。在编写模板代码时,应该尽可能使用这些新特性,以减少模板实现的复杂度和提高代码质量。

总的来说,C++模板是一种非常强大和灵活的编程技术,可以使代码更加通用、高效和易于维护。在编写模板代码时,我们应该注重代码的可读性和可维护性,以确保代码的质量和稳定性。


总结C++的模板template编程技术
https://qiangsun89.github.io/2023/04/24/总结C-的模板template编程技术/
作者
Qiang Sun
发布于
2023年4月24日
许可协议