博客主页:Skylar Lin
望本文能够给您带来一定的帮助,如果有错误的地方敬请斧正!
新人博主🧑,希望多多支持🍺,还有好多库存和大家分享🎁。
转载需注明出处和原作🌹。
前言
泛型编程的核心思想是将代码与特定数据类型解耦,使得代码能够适用于多种数据类型,从而实现更高程度的抽象。而这种思想在C++中通过模板技术得以实现,其中包括了函数模板和类模板。使用模板,我们可以编写一次代码,然后在需要的地方根据实际数据类型进行实例化,从而获得专门针对特定数据类型优化的代码。
在本文中,我总结了C++中函数模板和类模板的使用方法,以及如何通过模板特化实现对特定数据类型的定制化处理。通过阅读本文,您一定能够对泛型编程有个不错的了解。
函数模板
函数模板允许我们定义通用的函数,其中的类型参数可以在调用时被实际的数据类型所替代。函数模板使用关键字 template
声明,并在尖括号中指定类型参数。
#include <iostream>
using namespace std;
template <typename T>
T my_max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int result1 = my_max(10, 20); // 实例化 max<int>(10, 20)
double result2 = my_max(3.14, 2.71); // 实例化 max<double>(3.14, 2.71)
int result3 = my_max<int>(13, 0.14); // 正常,类型不一样,但可以发生隐式类型转换
cout << result1 << endl;
cout << result2 << endl;
cout << result3 << endl; // 输出 13
return 0;
}
函数模板在调用时,通过自动类型推导可以避免显式指定模板参数类型,但如果需要,我们也可以通过指定类型的方式进行调用。
函数模板的重载
函数模板也可以像普通函数一样进行重载。当普通函数和函数模板都能匹配函数调用时,编译器会优先选择普通函数。只有在没有找到适合的普通函数时,才会实例化函数模板。
#include <iostream>
int max(int a, int b) {
return (a > b) ? a : b;
}
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
template <typename T>
T max(T a, T b, T c) {
if (a > b && a > c)
return a;
else if (b > a && b > c)
return b;
else
return c;
}
int main() {
int result1 = max(10, 20); // 调用普通函数 max(int, int)
double result2 = max(3.14, 2.71); // 实例化 max<double>(3.14, 2.71)
int result3 = max(10, 20, 30); // 没有找到适合的普通函数,实例化函数模板
std::cout << result1 << std::endl;
std::cout << result2 << std::endl;
std::cout << result3 << std::endl;
return 0;
}
类模板
类模板允许我们定义通用的类,其中的类型参数可以在创建对象时被实际的数据类型所替代。类模板使用关键字 template
声明,并在尖括号中指定类型参数。
#include <iostream>
template <typename T, typename U = int>
class MyContainer {
private:
T data1;
U data2;
public:
MyContainer(T value1, U value2) : data1(value1), data2(value2) {}
T getData1() const;
U getData2() const;
};
template <typename T, typename U>
T MyContainer<T, U>::getData1() const {
return data1;
}
template <typename T, typename U>
U MyContainer<T, U>::getData2() const {
return data2;
}
int main() {
MyContainer<double, double> container1(1.3, 1.4); // 实例化 MyContainer<double, double>
MyContainer<double> container2(1.3, 1.4); // 实例化 MyContainer<double, int>
auto value1 = container1.getData1();
auto value2 = container1.getData2();
auto value3 = container2.getData1();
auto value4 = container2.getData2();
std::cout << value1 << "和" << value2 << std::endl; // 输出 1.3和1.4
std::cout << value3 << "和" << value4 << std::endl; // 输出 1.3和1
return 0;
}
类模板中成员函数的创建时机
普通类中成员函数在类定义阶段创建,但类模板中的成员函数在调用时才会创建。
- 类模板中成员函数的定义实际上是模板的描述,告诉编译器如何生成函数代码,但不会创建具体函数代码。
- 在使用类模板创建对象时,根据模板实参,编译器会实际生成成员函数的实现代码,这称为成员函数的实例化。
template <typename T>
class MyContainer {
public:
void print(T value) {
std::cout << "Value: " << value << std::endl;
}
};
int main() {
MyContainer<int> intContainer; // 创建 MyContainer<int> 对象
intContainer.print(42); // 实例化 print<int>(int value),输出 "Value: 42"
MyContainer<double> doubleContainer; // 创建 MyContainer<double> 对象
doubleContainer.print(3.14); // 实例化 print<double>(double value),输出 "Value: 3.14"
return 0;
}
类模板对象作为函数参数
使用类模板实例化出的对象作为参数传递给函数有三种方式:
- 指定传入类型: 直接显示对象的数据类型(最常用)
- 参数模板化: 将对象中的参数变为模板进行传递
- 整个类模板化: 将对象类型作为模板参数进行传递
#include<iostream>
#include<string>
using namespace std;
template<class T1,class T2>
class Person {
public:
Person(T1 name,T2 age) {
this->m_Name = name;
this->m_Age = age;
}
void showPerson() {
cout << "姓名:" << this->m_Name << endl;
cout << "年龄:" << this->m_Age << endl;
}
T1 m_Name;
T2 m_Age;
};
// 1. 指定传入类型
void printPerson1(Person<string,int> &p) {
p.showPerson();
}
void test01() {
Person<string,int> p ("孙悟空",999);
printPerson1(p);
}
// 2. 参数模板化(函数模板)
template<class T1,class T2>
void printPerson2(Person<T1,T2> &p) {
p.showPerson();
cout << "T1的类型为:" << typeid(T1).name() << endl;
cout << "T2的类型为:" << typeid(T2).name() << endl;
}
void test02() {
Person<string,int> p ("猪八戒",888);
printPerson2(p);
}
// 3. 整个类模板化
template<class T>
void printPerson3(T &p) {
p.showPerson();
cout << "T的类型为:" << typeid(T).name() << endl;
}
void test03() {
Person<string,int> p ("唐三藏",777);
printPerson3(p);
}
int main() {
test01();
test02();
test03();
return 0;
}
类模板继承
当派生类继承的基类是一个类模板时,派生类在声明时需要明确指定基类中T
的类型。这是因为编译器需要在编译时为派生类分配内存。
#include <iostream>
template <typename T>
class Base {
protected:
T data;
public:
Base(T value) : data(value) {}
void printData() {
std::cout << "Base: " << data << std::endl;
}
};
// 派生类模板继承父类模板
template <typename T>
class Derived : public Base<T> {
private:
T extraData;
public:
Derived(T value1, T value2) : Base<T>(value1), extraData(value2) {}
void printExtraData() {
std::cout << "Derived: " << this->extraData << std::endl;
}
};
int main() {
Derived<int> obj(10, 20);
obj.printData(); // 输出 "Base: 10"
obj.printExtraData(); // 输出 "Derived: 20"
return 0;
}
如果想在基类中灵活指定T
的类型,需要将派生类写成类模板。
模板特化
在C++中,可以针对特定的类型提供特定的模板实现,称为模板特化(Template Specialization)。模板特化允许我们为某些特定类型提供定制的实现,以便在使用这些类型作为模板参数时,编译器可以选择特化版本而不是通用的模板版本。模板特化有两种形式:完全特化和偏特化。
- 完全特化是对模板的完整特化,使用
template <>
语法,尖括号中指定具体的类型。 - 偏特化是对模板的部分特化,对一些模板参数提供特定的实现,但保留其他模板参数的通用实现。偏特化适用于函数模板的部分特殊情况。
函数模板特化
#include <iostream>
template <typename T>
T add(T a, T b) {
return a + b;
}
template <>
int add(int a, int b) {
return a * b;
}
template <typename T, typename U>
void print(T a, U b) {
std::cout << a << " " << b << std::endl;
}
template <typename U>
void print(int a, U b) {
std::cout << a * 2 << " " << b << std::endl;
}
int main() {
int result1 = add(10.0, 20.0); // 使用通用模板,返回 30
int result2 = add(10, 20); // 使用特化,返回 200
std::cout << result1 << std::endl;
std::cout << result2 << std::endl;
print(1.3, 1.4); // 使用通用模板,输出 "1.3 1.4"
print(13, 14); // 使用偏特化,输出 "26 14"
return 0;
}
类模板特化
关于类模板的特化,C++实际上只支持对成员函数进行特化,而不支持对整个类进行完全特化或偏特化。
#include <iostream>
template <typename T>
class MyContainer {
public:
void print() {
std::cout << "Generic version" << std::endl;
}
};
template <>
void MyContainer<int>::print() {
std::cout << "Specialization for int" << std::endl;
}
int main() {
MyContainer<char> obj1; // 使用通用模板,输出 "Generic version"
MyContainer<int> obj2; // 使用特化,输出 "Specialization for int"
obj1.print();
obj2.print();
return 0;
}
SFINAE原则
当使用模板时,C++会尝试根据函数参数的类型来推导模板参数类型,但有时可能会遇到多个模板函数都可以匹配传递的参数,这会导致模板重载冲突。这时,SFINAE原则就派上用场了。
在C++中,SFINAE(Substitution Failure Is Not An Error)原则是一种模板元编程技术。它允许编译器在模板实例化时忽略一些错误,从而使得我们能够根据条件来选择合适的模板实例。
在使用SFINAE原则时,你需要了解以下几个关键要点:
-
头文件引入: 需要引入
<type_traits>
头文件,以便使用其中的类型特性和模板工具。 -
条件约束: 使用
std::enable_if_t
模板结合类型特性来对模板参数进行条件约束。类型特性(如
std::is_integral_v
、std::is_floating_point_v
等)可以判断模板参数是否满足特定条件,从而影响模板参数的推导结果。 -
模板参数默认值: 在模板参数列表中使用
typename = ...
或class = ...
指明约束条件。比如:
<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
-
使用时机: 在 函数重载 或 模板特化 时使用。
下面是一个简单的例子,演示了如何使用SFINAE原则:
#include <iostream>
#include <type_traits>
// 使用 SFINAE 原则实现的模板重载
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) {
std::cout << "Processing integral value: " << value << std::endl;
}
template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
void process(T value) {
std::cout << "Processing floating-point value: " << value << std::endl;
}
int main() {
process(42); // 调用第一个 process 函数
process(3.14); // 调用第二个 process 函数
// process("Hello"); // 编译错误,没有匹配的函数
return 0;
}
C++20中的新特性
C++20 引入了许多新特性,**概念(Concepts)**就是其中之一。概念同样允许我们在模板中添加约束条件。
概念的使用可以分为以下几个步骤:
-
定义概念: 使用
template <typename T> concept ConceptName = ...
的形式,在等号后面使用表达式来描述约束的条件。 -
使用概念: 在模板参数列表中使用约束的语法是:
template <ConceptName T>
,该模板参数与指定的概念进行关联。 -
编写模板代码: 在编写模板函数或类时,可以在模板参数列表中使用概念来约束参数类型。这样,在调用模板函数或实例化模板类时,只有满足概念约束的参数才会成功。
#include <iostream>
#include <concepts>
// 定义一个概念,约束类型必须为整数
template <typename T>
concept IntegralType = std::is_integral_v<T>;
// 使用概念约束函数模板
template <IntegralType T>
void printOnlyIntegersWithConcept(T value) {
std::cout << value << " is an integer." << std::endl;
}
int main() {
printOnlyIntegersWithConcept(42); // 调用成功,满足IntegralType概念
// printOnlyIntegersWithConcept(3.14); // 编译错误,不满足IntegralType概念
return 0;
}
更多高级主题
除了基础的函数模板和类模板,泛型编程领域还存在许多高级主题,值得我们深入探讨。在今后有时间 Lin 也会总结出来和大家分享。