泛型编程基础知识详解
函数模板
模板的定义包含几个关键字的使用
-
使用
template
关键字开头 -
类型模板参数
T
前面要是用typename
修饰。template <typename T> T add(T v_1, T v_2) { return v_1 + v_2; }
参数的推断
指定模板类型
编译器可以根据输入自行推断参数的数据类,但是要显示的输入否则无法推断,如下所示
template <typename T, typename U, typename R>
R add(T v_1, U v_2) {
return v_1 + v_2;
}
int main() {
std::cout << add(1, 1.2) << std::endl;
return 0;
}
上面的代码定义了,三个类型模板参数其中T 明确是 int
而U 明确是 double
,但是由于没有信息明确指出返回值R 是什么类型
,因此,在编译时编译器会报如下错误信息。
error C2672: “add”: 未找到匹配的重载函数
error C2783: “R add(T,U)”: 未能为“R”推导 模板 参数
这时可以使用<> 明确的指出每个模板参数的类型
,如下所示。
int main() {
std::cout << add<int, double, double>(1, 1.2) << std::endl;
return 0;
}
如果者写法比较麻烦可以优化代码,指定一部分模板参数
,让编译器推断另外一部分模板参数
,修改如下。
template <typename R, typename T, typename U>
R add(T v_1, U v_2) {
return v_1 + v_2;
}
int main() {
std::cout << add<double>(1, 1.2) << std::endl;
return 0;
}
上面的代码说明了<>
指定模板参数类型的规则,明确模板参数的类型,一旦从某个模板参数开始推断,后续的所有模板参数都需要让编译器推断,不能跳着指定,有点儿类似与函数写默认参数的规则。
除了指定模板类型参数的方法外,还可以使用返回类型后置的方法,如下所示:
template <typename T, typename U>
auto add(T v_1, U v_2)->decltype(v_1 + v_2) {
return v_1 + v_2;
}
类型推断总结
-
根据实际类型推断
template <typename T> T add(T v_1, T v_2) { return v_1 + v_2; } int main() { std::cout << add<double>(1.2, 1.2) << std::endl; return 0; }
-
指定参数类型
template <typename R, typename T, typename U> R add(T v_1, U v_2) { return v_1 + v_2; } int main() { std::cout << add<double>(1, 1.2) << std::endl; return 0; }
函数模板特化
全特化
全特化就是将泛化函数模板中的所有模板参数都用具体的类型代替,构成一个特殊的函数模板。
#include <iostream>
/* 泛化 */
template <typename T, typename U>
void test(T & v_1, U & v_2) {
std::cout << "template func" << std::endl;
}
/* 全特化 */
template<>
void test<int, double>(int & v_1, double & v_2) {
std::cout << "over specialized func" << std::endl;
}
这种全特化的模板函数会被正常函数覆盖,如下所示。
#include <iostream>
template <typename T, typename U>
void test(T & v_1, U & v_2) {
std::cout << "template func" << std::endl;
}
template<>
void test<int, double>(int & v_1, double & v_2) {
std::cout << "over specialized func" << std::endl;
}
void test(int & v_1, double & v_2) {
std::cout << "normal func" << std::endl;
}
int main() {
int v_1 = 1; double v_2 = 1.2;
test(v_1, v_2); // 输出 normal func
return 0;
}
类模板
类模板,也是产生类的模具,通过给定的模板参数,生成具体的类,也就是实例化一个特定的类。类模板声明和实现
都放在一个头文件中,因为实例化具体类的时候必须有类模板的全部信息。
#include <iostream>
template <typename T>
class TempClass {
public:
TempClass(T & _val);
};
template<typename T>
TempClass<T>::TempClass(T &_val) {
std::cout << _val << std::endl;
}
int main() {
int v_1 = 1; double v_2 = 1.2;
TempClass<int> t_1(v_1);
// TempClass<int> t_2(v_2); 编译报错
TempClass<double> t_2(v_2);
return 0;
}
参数的推断
在C++11
中不能直接通过传入的参数类型推断出类模板的类型只能显示的声明,但是在C++17
中则可以直接像函数模板那样直接推断类型,如下所示。
#include <iostream>
template <typename T>
class TempClass {
public:
TempClass(T & _val);
};
template<typename T>
TempClass<T>::TempClass(T &_val) {
std::cout << _val << std::endl;
}
int main() {
int v_1 = 1; double v_2 = 1.2;
TempClass<double> t_2(v_2);
TempClass t_1(v_1); // c++11 中编译报错
return 0;
}
类模板的特化
全特化
类的泛化完整的写法如下,在每个函数的实现前加template<typename T>
模板关键字,或者直接在声明处进行代码编写。
#include <iostream>
template <typename T>
class TempClass {
public:
void printTempClass();
};
template<typename T>
void TempClass<T>::printTempClass() {
std::cout << "template func" << std::endl;
}
int main() {
TempClass<int> s;
s.printTempClass();
return 0;
}
泛化模板类与全特化模板类是完全独立的。
template <>
class TempClass<int> {
public:
TempClass();
};
TempClass<int>::TempClass() {
std::cout << "over specialized TempClass" << std::endl;
}
int main() {
TempClass<int> s;
s.printTempClass(); // 报错
return 0;
}
由于TempClass<int>
属于全特化的模板,可以看作是一个全新的类
,但是这个类模板中没有声明printTempClass()
函数,因此编译时出错。
如果在写全特化类模板时需要使用函数声明和实现分离的方式,则在函数实现前不用添加template<>
关键字,这是由于全特化类模板相当于一个普通类,因此,需要按照普通类的成员函数进行实现,代码如下。
template <>
class TempClass<int> {
public:
void printTempClass();
};
// template<> 不能写
void TempClass<int>::printTempClass() {
std::cout << "over specialized func" << std::endl;
}
成员函数全特化
如果只是需要全特化类模板中的某个成员变量则在实现函数前添加template<>
关键字,如下所示。
#include <iostream>
template <typename T>
class TempClass {
public:
void printTempClass();
};
template<>
void TempClass<double>::printTempClass() {
std::cout << "over specialized func double" << std::endl;
}
template<>
void TempClass<int>::printTempClass() {
std::cout << "over specialized func int" << std::endl;
}
int main() {
TempClass<double> temp_class_double;
temp_class_double.printTempClass();
TempClass<int> temp_class_int;
temp_class_int.printTempClass();
return 0;
}
成员函数模板
普通函数
和类模板
都可以定义自己的函数模板
,如下所示代码。
// 类模板
template <typename T>
class TempClass {
public:
template<class T2> // 定义自己的模板
TempClass(T2 & _value);
};
template<typename T> // 必需先写类模板标记
template<class T2>
TempClass<T>::TempClass(T2 &_value) {
}
需要注意的是,如果是类模板的成员函数模板,在实现时需要先写类模板的标记关键字
。
// 普通类
class TempClass {
public:
template<class T2> // 定义自己的模板
TempClass(T2 & _value);
};
template<class T2>
TempClass::TempClass(T2 &_value) {
}
模板中友元
友元类
// 前置声明
template <typename T> class TempClassFriend;
template <typename T>
class TempClass {
friend class TempClassFriend<int>; // 声明友元时特化友元类
private:
int age_;
};
template <typename T>
class TempClassFriend {
public:
void TestFriend() {
TempClass<int> temp;
temp.age_ = 10; // 正常调用
}
};
template <typename T>
class TempClass {
template <typename> friend class TempClassFriend; // 将类模板直接声明成友元类
private:
int age_;
};
template <typename U>
class TempClassFriend {
public:
void TestFriend() {
TempClass<int> temp{};
temp.age_ = 10; // 正常调用
std::cout << temp.age_ << std::endl;
}
};
template <typename T>
class TempClass {
friend T; // 将需要声明为友元类的类模板变成模板类型参数
private:
int age_;
};
template <typename U>
class TempClassFriendA {
public:
void TestFriend() {
TempClass<TempClassFriendA> temp{}; // 将类作为类型参数之一
temp.age_ = 10; // 正常调用
std::cout << temp.age_ << std::endl;
}
};
友元函数
#include <iostream>
template <typename U> void func(U _value); // 模板前置声明
class TempClass {
friend void func<int>(int); // 声明特化版本函数模板
private:
int age_ = 10;
};
template <typename U>
void func(U _value) {
TempClass temp;
U value = temp.age_ + _value;
std::cout << value << std::endl;
}
#include <iostream>
class TempClass {
template <typename U> friend void func(U _value); // 定义声明函数模板为友元函数
private:
int age_ = 10;
};
template <typename U>
void func(U _value) {
TempClass temp;
U value = temp.age_ + _value;
std::cout << value << std::endl;
}
可变参函数模板
可变参函数模板最基本写法如下:
#include <iostream>
#include <vector>
template <typename... T>
void changeParam(T... args) {
std::cout << sizeof...(args) << std::endl; // 推断参数数量
}
- 尖括号中的
typename
后跟三个点。 - 原模板形参
T称为可变参数类型
或称为包
,它包含了0到多个不同类型参数。
可变参数(包)的展开
包数量和类型是不固定的因此需要将包中的数据一次取出就需要将包进行参数包展开
,参数包的展开也是比较固定的方法,采用递归的方式进行展开
。
可变参函数模板的代码在编写时需要有两个函数作为辅助:
- 参数包展开函数。
- 同名的递归终止函数,该函数是一个普通函数,不是函数模板,且必须是空参。
// 可变参函数模板声明
template <typename T, typename... U>
void changeParam(T param, U... args) {
std::cout << "param = " << param << std::endl;
changeParam(args...);
}
// 递归终止函数
void changeParam() {
std::cout << "package over." << std::endl;
}
int main() {
changeParam(10, 2.6, "abc");
return 0;
}
运行结果
param = 10
param = 2.6
param = abc
package over.
递归过程解析:
- 第一次执行
changeParam(10, 2.6, "abc")
时,T param = 10
而U... args = 2.6, "abc"
,然后进入第二次。 - 第二次执行属于递归过程,函数的入参有两个,当传入一个包作为参数时,编译器自动将包中的第一个参数分出来,因此,
T param = 2.6
而U... args = "abc"
,然后进入第三次。 - 第三次执行
T param = "abc"
而U... args已经没有参数
,进入第四次。 - 第四次执行时由于
args没有参数
,因此变为了空参,此时编译器会调用void changeParam()
终止函数。
是在c++17
标准中增加了一个编译期间if语句
,可以在编译时判断常量,用这种语法可以省掉递归终止函数
,写法如下:
template <typename T, typename... U>
void changeParam(T param, U... args) {
std::cout << "param = " << param << std::endl;
if constexpr(sizeof...(args) > 0) {
changeParam(args...);
} else {
std::cout << "package over." << std::endl;
}
}
折叠表达式
折叠表达式(Fold Expressions)
是C++17标准中新加入的,可以直接对可变长参数进行计算,示例代码如下:
template <typename ...T>
auto add(T ...args) {
return (... + args);
}
int main() {
std::cout << add(10, 20, 30) << std::endl;
return 0;
}
一元左折
表示从左开始计算,计算顺序(([0] 运算符 [1]) 运算符 [2]) 运算符 ...... 运算符 [n]
。
template <typename ...T>
auto sub(T ...args) {
return (... - args); // 点在左边
}
int main() {
std::cout << sub(10, 20, 30) << std::endl; // (10 - 20) - 30 = -40
return 0;
}
一元右折
表示从右开始计算,计算顺序0 ...... 运算符 ([n - 2] 运算符 ([n - 1] 运算符 [n]))
。
template <typename ...T>
auto sub(T ...args) {
return (args - ...); // 点在右边
}
int main() {
std::cout << sub(10, 20, 30) << std::endl; // 10 - (20 - 30) = -20,
// 这里一定不能想象成 30 - 20 - 10
return 0;
}
二元左折
在一元左折的基础上增加了一个初始值的设定,计算前先和这个初始值进行计算。
template <typename ...T>
auto sub(T ...args) {
return (100 - ... - args); // 注意初始值和点之间也要有符号,符号必须一致
}
int main() {
std::cout << sub(10, 20, 30) << std::endl; // (((100 - 10) - 20) - 30) = 40
return 0;
}
二元右折
template <typename ...T>
auto sub(T ...args) {
return (args - ... - 100); // 注意初始值和点之间也要有符号,符号必须一致
}
int main() {
std::cout << sub(10, 20, 30) << std::endl; // (10 - (20 - (30 - 100))) = -80
return 0;
}
可变参数表达式
让每个参数都自行计算,例如,可以配和折叠表达式让所有数值都扩大2倍然后在求和。
template <typename ...T>
auto sub(T ...args) {
return (args + ...);
}
template <typename ...T>
auto multiple(T ...args) {
return sub(2 * args...); // 每个参数都扩大两倍
}
int main() {
std::cout << multiple(10, 20, 30) << std::endl; // 120
return 0;
}
可变参类模板
类型展开
template <typename ...Args>
class testClass {
public:
testClass() {
printf("Template, this = %p\n", this);
}
};
template <typename T, typename ...Args>
class testClass<T, Args...>
: private testClass<Args...>{
public:
testClass(): data_(0) {
std::cout << "PartialSpecial, this = "
<< this << ", sizeof... (args) = "
<< sizeof... (args) << std::endl;
}
private:
T data_;
};
int main() {
testClass<int, float, double> tc;
return 0;
}
Template, this = 00000095200FF698
PartialSpecial, this = 00000095200FF698, sizeof... (args) = 0
PartialSpecial, this = 00000095200FF698, sizeof... (args) = 1
PartialSpecial, this = 00000095200FF698, sizeof... (args) = 2
-
结果分析:
- 当创建
testClass
对象的时候,有三个参数类型,和函数解包时一样,T得到一个类型T = int
,因此Args还有2个类型
,进入继承父类构造开始递归。 - 由于
Args还有2个类型
,因此,继承的是有2个类型参数的类模板,调用其构造函数后,T得到一个类型T = float
而Args还有1个类型
,再次递归构造其父类。 - 由于
Args还有1个类型
,因此,继承的是有1个类型参数的类模板,调用其构造函数后,T得到一个类型T = double
而Args还有0个类型
,再次递归构造其父类。 - 当构造无参的类模板构造函数时,就进入了
终止递归类模板(主模板)
,根据继承时的构造关系这个模板也是最先实例化的最终得到如上输出。
- 当创建
-
写法规范
-
不能出现一个以上的可变参数类型。
template <typename ...Args1, typename ...Args2> class testClass {};
-
有多个类型时将可变参数模板放在最后。
template <typename T, typename ...Args> class testClass {};
-
模板类型展开
如果类模板的传入参数都是容器,则需要用下列方法进行展开。
// [3] 递归展开终止函数
template <typename T, template<typename> typename ...Container>
class testClass {
public:
testClass() {
printf("Template, this = %p\n", this);
}
};
// [2] 类模板递归展开函数
template <typename T,
template<typename> typename FirstContainer,
template<typename> typename ...Container>
class testClass<T, FirstContainer, Container...>
: private testClass<T, Container...> {
public:
testClass() {
std::cout << "PartialSpecial, this = "
<< this << ", sizeof... (Container) = "
<< sizeof... (Container)
<< std::endl;
container_.push_back(10);
}
private:
FirstContainer<T> container_;
};
// [1] 要展开的类模板
template <typename T,
template<typename> typename ...Container>
class myClass
: private testClass<T, Container...> {
public:
myClass() {
std::cout << "myClass, this = "
<< this << ", T type is = "
<< typeid(T).name() << ", container size = "
<< sizeof... (Container)
<< std::endl;
}
};
int main() {
myClass<int, std::vector, std::list, std::deque> tc;
return 0;
}
Template, this = 0000009262AFFD00
PartialSpecial, this = 0000009262AFFD00, sizeof... (Container) = 0
PartialSpecial, this = 0000009262AFFD00, sizeof... (Container) = 1
PartialSpecial, this = 0000009262AFFD00, sizeof... (Container) = 2
myClass, this = 0000009262AFFD00, T type is = int, container size = 3