目录
函数模板
类模板
变量模板
模板全特化
模板偏特化
模板显式实例化解决文件分离问题
折叠表达式
模板的二阶段编译
待决名(dependent name)
SFINAE
概念与约束
函数模板
函数模板不是函数,只有实例化的函数模板,编译器才能生成实际的函数定义,不过在很多时候,它看起来就像普通函数一样。
示例:
下面是一个函数模板,返回两个对象中较大的那个:
template<typename T>
T max(T a,T b){return a > b ? a : b;
}
这里其实对T有几个要求,1:有>运算符,比如内置的int,double; 2:返回了一个T,即要求T是可以移动或复制的。 如果函数模板实参不满足以上要求,则匹配不到此模板。
C++17 之前,类型 T 必须是可复制或移动才能传递参数。C++17 以后,即使复制构造函数和移动构造函数都无效,因为 C++17 强制的复制消除的存在,也可以传递临时纯右值。
使用模板
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>
}
模板函数可手动指定模板形参类型,也可以让编译器推导,即模板参数推导(template argument deduction),c++11支持函数模板参数推导,但是类模板参数推导要到c++17才支持。
对于编译无法推导的场景,可手动指定,如:
max<double>(1, 1.2);
max<std::string>("luse"s, "乐");
但是 std::string 没有办法如此操作,编译器会报:
<source>:31:4: error: call of overloaded 'max(std::string, std::string)' is ambiguous
31 | max(string("luse"), string("乐"));
| ~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:23:3: note: candidate: 'T max(const T&, const T&) [with T = std::__cxx11::basic_string<char>]'
23 | T max(const T& a, const T& b) {
| ^~~
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/bits/stl_algobase.h:257:5: note: candidate: 'constexpr const _Tp& std::max(const _Tp&, const _Tp&) [with _Tp = __cxx11::basic_string<char>]'
257 | max(const _Tp& __a, const _Tp& __b)
即函数有二义性,这是因为自己所编写的max函数和标准库的max函数冲突了,但是int double实例化max函数并不会。原因是string在std命名空间,标准库的max函数也在std命名空间,虽然我们没有使用 std::
,但是根据 C++ 的查找规则,(实参依赖查找)ADL,依然可以查找到。
那么我们如何解决呢?很简单,进行有限定名字查找,即使用 ::
或 std::
说明,你到底要调用 “全局作用域”的 max,还是 std 命名空间中的 max。
::max(string("luse"), std::string("乐"));
有默认实参的模板类型形参
就如同函数形参可以有默认值一样,模板形参也可以有默认值。
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::stringreturn a > b ? a : b;
}int main(){auto ret = ::max("1", "2"s);std::cout << ret << '\n';
}
让 max 函数模板接受两个参数的时候不需要再是相同类型,那么这自然而然就会引入另一个问题了,如何确定返回类型?
typename RT = decltype(true ? T1{} : T2{})
这是一个三目运算符表达式。然后外面使用了 decltype 获取这个表达式的类型,那么问题是,为什么是 true 呢?以及为什么需要 T1{},T2{} 这种形式?
1:我们为什么要设置为 true?
其实无所谓,设置 false 也行,true 还是 false 不会影响三目表达式的类型。这涉及到了一些复杂的规则,简单的说就是三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 “公共”类型。
比如第二项是 int 第三项是 double,三目表达式当然会是 double。
using T = decltype(true ? 1 : 1.2);
using T2 = decltype(false ? 1 : 1.2);
2:为什么需要 T1{}
,T2{}
这种形式?
没有办法,必须构造临时对象来写成这种形式,这里其实是不求值语境,我们只是为了写出这样一种形式,让 decltype 获取表达式的类型罢了。
模板的默认实参的和函数的默认实参大部分规则相同。
decltype(true ? T1{} : T2{})
解决了。
事实上上面的写法都十分的丑陋与麻烦,我们可以使用 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 后置返回类型,它和我们之前用默认模板实参 RT
的区别只是稍微好看了一点吗?
不,它们的返回类型是不一样的,如果函数模板的形参是类型相同 true ? a : b
表达式的类型是 const T&
;如果是 max(1, 2)
调用,那么也就是 const int&
;而前面的例子只是 T
即 int
(前面都是用模板类型参数直接构造临时对象,而不是有实际对象,自然如此,比如 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
类型。即直接写auto会丢弃CV限定符,但decltype(auto)按decltype的规则推导类型,会保留CV限定符。
非类型模板形参
既然有”类型模板形参“,自然有非类型的,顾名思义,也就是模板不接受类型,而是接受值或对象。
template<std::size_t N>
void f() { std::cout << N << '\n'; }f<100>();
非类型模板形参有众多的规则和要求,目前,你简单认为需要参数是“常量”即可。
非类型模板形参当然也可以有默认值:
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"); // 匹配到模板
- 通常优先选择非模板的函数。
可变参数模板
和其他语言一样,C++ 也是支持可变参数的,我们必须使用模板才能做到。
老式 C 语言的变长实参有众多弊端,参见。
同样的,它的规则同样众多繁琐,我们不会说太多,以后会用到的,我们当前还是在入门阶段。
我们提一个简单的需求:
我需要一个函数 sum,支持 sum(1,2,3.5,x,n...) 即函数 sum 支持任意类型,任意个数的参数进行调用,你应该如何实现?
首先就要引入一个东西:形参包
本节以 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 args2f(args...); // 相当于 f(args0, args1, args2)f(&args...); // 相当于 f(&args0, &args1, &args2)
}int main() {sum("luse", 1, 1.2);
}