函数模板
定义模板
函数模板不是函数,只有实例化函数模板,编译器才能生成实际的函数定义。
声明一个函数模板,我们通常要使用:
template< 形参列表 > 函数声明
[!tip]
C++17 之前,类型 T 必须是可复制或移动才能传递参数。C++17 以后,即使复制构造函数和移动构造函数都无效,因为 C++17 强制的复制消除,也可以传递临时纯右值。
也可以使用 class
关键字来声明模板类型形参
使用模板
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
struct Test{
int v_{};
Test() = default;
Test(int v) :v_(v) {}
bool operator>(const Test& t) const{
return this->v_ > t.v_;
}
};
int main(){
int a{ 1 };
int b{ 2 };
std::cout << "max(a, b) : " << ::max(a, b) << '\n';
Test t1{ 10 };
Test t2{ 20 };
std::cout << "max(t1, t2) : " << ::max(t1, t2).v_ << '\n';
}
编译器会实例化两个函数,也就是生成了一个参数为 int 的 max 函数,一个参数为 Test 的函数。
例如
int max(int a, int b)
{
return a > b ? a : b;
}
Test max(Test a, Test b)
{
return a > b ? a : b;
}
- 模板,只有你“用”了它,才会生成实际的代码。
这里的“用”,其实就是指代会隐式实例化,生成代码。
并且需要注意,同一个函数模板生成的不同类型的函数,彼此之间没有任何关系。
[!tip]
显式的指明函数模板的形参类型
template<typename T> T max(T a, T b) { return a > b ? a : b; } int main(){ int a{ 1 }; int b{ 2 }; max(a, b); // 函数模板 max 被推导为 max<int> max<double>(a, b); // 传递模板类型实参,函数模板 max 为 max<double> }
模板参数推导
当使用函数模板(如 max())时,模板参数可以由传入的参数推导。
T 可能只是类型的“一部分”。若声明 max() 使用 const&
:
template<typename T>
T max(const T& a, const T& b) {
return a > b ? a : b;
}
如果我们 max(1, 2)
或者说 max<int>(x,x)
,T 当然会是 int,但是函数形参类型会是 const int&
。详见Deducing Types
[!warning]
有不少情况是没有办法进行推导的:即参数有多个类型并且没有显示指定类型
// 省略 max using namespace std::string_literals; int main(){ max(1, 1.2); // Error 无法确定你的 T 到底是要 int 还是 double max("luse"s, "乐"); // Error 无法确定你的 T 到底是要 std::string 还是 const char[N] }
需要显式指定函数模板的(T)类型。
max<double>(1, 1.2); max<std::string>("luse"s, "乐");
又或者说显式类型转换:
max(static_cast<double>(1), 1.2);
万能引用与引用折叠
所谓的万能引用(又称转发引用,或者通用引用),即接受左值表达式那形参类型就推导为左值引用,接受右值表达式,那就推导为右值引用。
[!tip]
通用引用不是一种新的引用,它实际上是满足以下两个条件下的右值引用:
- 类型推导区分左值和右值。
T
类型的左值被推导为T&
类型,T
类型的右值被推导为T
。- 发生引用折叠。
比如:
template<typename T>
void f(T&& t){}
int a = 10;
f(a); // a 是左值表达式,f 是 f<int&> 但是它的形参类型是 int&
f(10); // 10 是右值表达式,f 是 f<int> 但它的形参类型是 int&&
被推导为
f<int&>
涉及到了特殊的推导规则:如果 P 是到无 cv 限定模板形参的右值引用(也就是转发引用)且对应函数的调用实参是左值,那么将到 A 的左值引用类型用于 A 的位置进行推导。
通过模板或 typedef 中的类型操作可以构成引用的引用,此时适用引用折叠(reference collapsing)规则:
- 右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
template <class Ty>
constexpr Ty&& forward(Ty& Arg) noexcept {
return static_cast<Ty&&>(Arg);
}
int a = 10; // 不重要
::forward<int>(a); // 返回 int&& 因为 Ty 是 int,Ty&& 就是 int&&
::forward<int&>(a); // 返回 int& 因为 Ty 是 int&,Ty&& 就是 int&
::forward<int&&>(a); // 返回 int&& 因为 Ty 是 int&&,Ty&& 就是 int&&
有默认实参的模板类型形参
模板形参也可以有默认值
template<typename T = int>
void f();
f(); // 默认为 f<int>
f<double>(); // 显式指明为 f<double>
using namespace std::string_literals;
template<typename T1,typename T2,typename RT =
decltype(true ? T1{} : T2{}) >
RT max(const T1& a, const T2& b) { // RT 是 std::string
return a > b ? a : b;
}
int main(){
auto ret = ::max("1", "2"s);
std::cout << ret << '\n';
}
[!important]
typename RT = decltype(true ? T1{} : T2{})
RT声明为三目运算符表达式的类型
[!tip]
三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 “公共”类型。
比如第二项是 int 第三项是 double,三目表达式当然会是 double。
using T = decltype(true ? 1 : 1.2); using T2 = decltype(false ? 1 : 1.2);
T 和 T2 都是 double 类型。
利用auto 简化这一切。
template<typename T,typename T2> auto max(const T& a, const T2& b) -> decltype(true ? a : b){ return a > b ? a : b; }
C++11 后置返回类型
[!warning]
后置返回类型虽然也是写的
auto
,但是它根本没推导,只是占位。这和我们之前用默认模板实参
RT
的区别的返回类型是不一样的如果函数模板的形参是类型相同
true ? a : b
表达式的类型是const T&
使用 C++20 简写函数模板,我们可以直接再简化为:
decltype(auto) max(const auto& a, const auto& b) {
return a > b ? a : b;
}
C++14 引入了两个特性:
- 返回类型推导(也就是函数可以直接写 auto 或 decltype(auto) 做返回类型,而不是像 C++11 那样,只是后置返回类型。
decltype(auto)
“如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行”。我们上面的max
示例如果不使用 decltype(auto),按照模板实参的推导规则,会忽略引用和 cv 限定符,就只能推导出返回T
基本类型。
非类型模板形参
模板不接受类型,而是接受值或对象
非类型模板形参当然也可以有默认值:
template<std::size_t N = 100>
void f() { std::cout << N << '\n'; }
f(); // 默认 f<100>
f<66>(); // 显式指明 f<66>
目前,你简单认为需要参数是“常量”即可。
重载函数模板
函数模板与非模板函数可以重载。
template<typename T>
void test(T) { std::puts("template"); }
void test(int) { std::puts("int"); }
test(1); // 匹配到test(int)
test(1.2); // 匹配到模板
test("1"); // 匹配到模板
- 通常优先选择非模板的函数。
可变参数模板
[!important]
本节以 C++14 标准进行讲述。
模板形参包是接受零个或更多个模板实参(非类型、类型或模板)的模板形参。函数形参包是接受零个或更多个函数实参的函数形参。
template<typename...Args> void sum(Args...args){}
这样一个函数,就可以接受任意类型的任意个数的参数调用
模板中需要 typename 后跟三个点 Args,函数形参中需要用模板类型形参包后跟着三个点 再 args。
args 是函数形参包,Args 是类型形参包,它们的名字我们可以自定义。
args 里,就存储了我们传入的全部的参数,Args 中存储了我们传入的全部参数的类型。
void f(const char*, int, double) { puts("值"); } void f(const char**, int*, double*) { puts("&"); } template<typename...Args> void sum(Args...args){ // const char * args0, int args1, double args2 f(args...); // 相当于 f(args0, args1, args2) f(&args...); // 相当于 f(&args0, &args1, &args2) } int main() { sum("luse", 1, 1.2); }
sum 的
Args...args
被展开为const char * args0, int args1, double args2
。[!tip]
定义一个术语:模式。
后随省略号且其中至少有一个形参包的名字的模式会被展开 成零个或更多个逗号分隔的模式实例。
&args...
中&args
就是模式展开的时候,模式,也就是省略号前面的一整个表达式,会被不停的填入对象并添加
&
,然后逗号分隔。直至形参包的元素被消耗完。template<typename...Args> void print(const Args&...args){ // const char (&args0)[5], const int & args1, const double & args2 int _[]{ (std::cout << args << ' ' ,0)... }; } int main() { print("luse", 1, 1.2); }
模式是:
(std::cout << args << ' ' ,0)
[!warning]
- 只有在合适的形参包展开场所才能进行形参包展开。
template<typename ...Args> void print(const Args &...args) { (std::cout << args << " ")...; // 不是合适的形参包展开场所 Error! }
一个数组的示例:
template<typename...Args>
void print(const Args&...args) {
int _[]{ (std::cout << args << ' ' ,0)... };
}
//onst T(&array)[N] 注意,这是一个数组引用
template<typename T,std::size_t N, typename...Args>
void f(const T(&array)[N], Args...index) {
print(array[index]...); //array[index]... 是包展开
//array[index] 是模式
}
int main() {
int array[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
f(array, 1, 3, 5);
}
实现一个
sum
:#include <iostream> #include <type_traits> template<typename...Args,typename RT = std::common_type_t<Args...>> RT sum(const Args&...args) { RT _[]{ static_cast<RT>(args)... }; RT n{}; for (int i = 0; i < sizeof...(args); ++i) { // sizeof... 获取形参包的元素个数 n += _[i]; } return n; } int main() { double ret = sum(1, 2, 3, 4, 5, 6.7); std::cout << ret << '\n'; // 21.7 }
std::common_type_t
的作用很简单,就是确定我们传入的共用类型,说白了就是这些东西都能隐式转换到哪个,那就会返回那个类型。[!tip]
RT _[]{ static_cast<RT>(args)... };
创建一个数组,形参包在它的初始化器中展开,初始化这个数组,数组存储了我们传入的全部的参数。因为窄化转换禁止了列表初始化中 int 到 double 的隐式转换,所以我们需要显式的转换为“公共类型”
RT
。非类型模板形参也可以使用形参包
template<std::size_t... N> void f(){ std::size_t _[]{ N... }; // 展开相当于 1UL, 2UL, 3UL, 4UL, 5UL std::for_each(std::begin(_), std::end(_), [](std::size_t n){ std::cout << n << ' '; } ); } f<1, 2, 3, 4, 5>();
模板分文件
对模板进行分文件,写成 .h
.cpp
这种形式。
这显然是不可以的
#include "test.h"
#include "test_template.h"
int main(){
f(); // 非模板,OK
f_t(1); // 模板 链接错误
}
从头讲解编译链接,以及 #include
的知识
include 指令
预处理指令 #include
开始,就是简单的替换,但是不够明确
分文件的原理是什么?
通常将函数声明放在 .h
文件中,将函数定义放在 .cpp
文件中,我们只需要在需要使用的文件中 include
一个 .h
文件
事实上是把函数声明复制到了我们当前的文件中。
//main.cpp
#include "test.h"
int main(){
f(); // 非模板,OK
}
编译器在编译一个翻译单元(如 main.cpp)的时候,如果发现找不到函数的定义,那么就会空着一个符号地址,将它编译为目标文件。期待链接器在链接的时候去其他的翻译单元找到定义来填充符号。
不单单是函数,全局变量等都是这样,这是编译链接的基本原理和步骤。
类会有所不同,总而言之后续视频会单独讲解的。
不能模板不能分文件4的原因就显而易见了,我们在讲使用模板的时候就说了:
- 模板,只有你“用”了它,才会生成实际的代码。
单纯的放在一个 .cpp
文件中,它不会生成任何实际的代码,自然也没有函数定义,也谈不上链接器找符号了。
所以模板通常是直接放在
.h
文件中,而不会分文件。或者说用.hpp
这种后缀,这种约定俗成的,代表这个文件里放的是模板。
- 注:函数模板自身并不是类型、函数或任何其他实体。不会从只包含模板定义的源文件生成任何代码。模板只有实例化才会有代码出现。 ↩
- 注:术语“实例化”,指代的是编译器确定各模板实参(可以是根据传入的参数推导,又或者是自己显式指明模板的实参)后从模板生成实际的代码(如从函数模板生成函数,类模板生成类等),这是在编译期就完成的,没有运行时开销。实例化还分为隐式实例化和显式实例化,后面会详细聊。 ↩
- 注:“重载决议”,简单来说,一个函数被重载,编译器必须决定要调用哪个重载,我们决定调用的是各形参与各实参之间的匹配最紧密的重载。 ↩
- 注:这个问题可以通过显式实例化解决,在后面会讲。 [↩](https://github.com/Mq-b/Modern-Cpp-templates-tutorial/blob/main/md/第一部分-基础知识/01函数模板.md#user-content-fnref-4-3aff5140d04d84f7bex