使用template关键字引入模板
函数模板的显示实例化
fun<int>(3); // 会在终端打印出“3”
函数模板的重载
template<typename T>
void fun(T input){
std::cout << input << std::endl;
}
template<typename T>
void fun(T* input){
std::cout << *input << std::endl;
}
template<typename T, typename T2>
void fun(T input, T2 input2){
std::cout << input << std::endl;
std::cout << input2 << std::endl;
}
int x = 3;
fun<int>(&x);
fun<int>(x);
模版实参的类型推导
- 如果函数模版在实例化时没有显示制定模版实参,那么系统会尝试进行推导
- 推导时基于函数实参(表达式)确定模版实参的过程,其基本原则与auto类型推导类似
- 函数形参是左值引用/指针:
- 函数的形参是万能引用
- 函数形参不包含引用(写起来最简单,模型推导最复杂)
- 多个T情况
// 同样的T
template<typename T>
void fun(T input1, T input2) {}
int main() {
fun<int>(3, 5.0); // 通过,不存在模型推导,T显示指定为int
fun(3, 5.0); // 不通过,3推导出T为int,5.0推导出T为double,int和double不是一个类型,报错
}
// 不同的T
template<typename T>
void fun(T1 input1, T2 input2) {}
int main() {
fun<int>(3, 5.0); // 通过,T1显示指定为int,T2隐式指推断为double
}
模版实参并非总是能够推导得到
- 如果模版形参与函数形参无关,则无法推导
template <typename T, typename Res>
Res fun(T input) {}
int main() {
fun(3); // Res无法推导,编译不通过
}
- 即使相关,也不一定能进行推导,推导成功也可能存在因歧义而无法使用(见上面“同样的T”)
template <typename T>
void fun(typename std::remove_reference<T>::type input) {}
int main() {
fun(3); // 编译器在实例化fun函数的时候,发现std::remove_reference<T>::type的结果是int
// T到底是啥不确定,可以是int,也可以是int&,int&&所以编译器报错
}
在无法推导时,编译器会选择使用缺省模版实参
template <typename T = int>
void fun(typename std::remove_reference<T>::type input) {}
int main() {
fun(3); // 编译通过,有缺省值
}
显示指定部分模版实参
- 显示指定的模版实参必须是从最左边开始,依次指定
- 模版形参的声明顺序会影响调用的灵活性
- 越是需要显示指定的T,就越要放在前面
// good case
template <typename Res, typename T>
Res fun(T x) {}
int main() {
fun<int>(5); // Res显示指定,T隐式指定
}
// bad case
template <typename T, typename Res>
Res fun(T x) {}
int main() {
fun<int>(5); // T显示指定, Res无法推断
}
函数模板自动推导是会遇到的几种情况
- 函数形参无法匹配——SFINAE(替换失败并非错误)
template <typename T>
void fun(T x, T y) {}
int main() {
fun(3, 5.0); // 编译不通过,但是不代表template fun有错误,只是找不到合适的匹配模板
// 如果重载一个fun(T1, T2),就没问题了
// SFINAE这个概念在元编程的时候很有用
}
- 模板与非模版同时匹配,匹配等级相同,系统会选择非模版函数
- 多个模版同时匹配,此时采用偏序关系确定选择“最特殊”版本
template <typename T>
void fun(T x, float y) {
std::cout << 2 << std::endl;
}
template <typename T, typename T2>
void fun(T x, T2 y) {
std::cout << 1 << std::endl;
}
template <typename T>
void fun(T* x, float y) {
std::cout << 3 << std::endl;
}
int main() {
fun(3, 5.0); // 2
int x = 3;
fun(&x, 5.0); // 3
}
- 如果一样特殊,那就编译不通过了
template <typename T, typename T2>
void fun(T* x, T2 y) {}
template <typename T, typename T2>
void fun(T x, T2* y) {}
int main() {
int x = 3;
fun(&x, &x);
}
模版函数的实例化控制
- 只实例化,不调用函数
- 某些库中不想给出模版的内部实现逻辑,只给出模版的声明,此时就需要提前实例化用户想要使用的函数
- 显示实例化定义
// header.h
template <typename T>
void fun(T x) {
std::cout << x << std::endl;
}
template
void fun<int>(int); // 实例化
// 也可以这么写:void fun(int);
// main.cc
#include "header.h"
int main() {
int x = 3;
fun<int>(x);
}
- 显示实例化声明
//header.h
template <typename T>
void fun(T x) {
std::cout << x << std::endl;
}
// source.cc
#include "header.h"
template
void fun<int>(int);
// main.cc
#include "header.h"
extern template
void fun<int>(int); // extern表示已经在别的翻译单元实例化过了,这里不要再实例化一遍
// 链接的时候会直接链到source里面的实例
int main() {
int x = 3;
fun<int>(x);
}
- 注意一处定义原则
- 隐式实例化可以在多处有实例化,编译器会选择其中一个,删除掉多余的
// header.h
template <typename T>
void fun(T x) {
std::cout << x << std::endl;
}
// source.cc
#include "header.h"
void fun2() {
fun<int>(3); // source中隐式实例化一次
}
// main.cc
#include "header.h"
int main() {
int x = 3;
fun<int>(x); // main中隐式实例化一次
}
- 显示实例化原则上在整个程序中只能有一处,但是有多处的话,编译器也不一定会报错,尽量不要这么写
// header.h
template <typename T>
void fun(T x) {
std::cout << x << std::endl;
}
// source.cc
#include "header.h"
template
void fun<int>(int); // source中显示实例化一次
void fun2() {
fun<int>(3);
}
// main.cc
#include "header.h"
template
void fun<int>(int); // main中显示实例化一次
int main() {
int x = 3;
fun<int>(x);
}
- 注意实例化过程中的模版形参推导
// 模版1
template <typename T>
void fun(T x) {
std::cout << x << std::endl;
}
template
void fun(int* x); // 显示实例化放在这里会调用模板1,因为程序从上到下执行,还没看到后面的模板2
// 模板2
template <typename T>
void fun(T* x) {
std::cout << x << std::endl;
}
template
void fun(int* x); // 显示实例化放在这里会调用模板2
模版函数的(完全)特化
补充
- (C++20)函数模板的简化形式:使用auto定义模板参数类型
- 优势:书写简洁
- 劣势:在函数内部需要间接获取参数类型信息