-
c++模板包括:类模板、类(非模板类和模板类)方法模板、函数模板、别名模板、变量模板。
类模板模板参数列表说明:
1)类定义
仅模板参数列表声明,template<>行。
类名后无需参数说明<>。
2)类方法定义
在类外面定义方法时,方法名前需要类型限定:类名后面需要类型说明<>。包括所有未特化模板参数和特化模板参数。
可能:全都是未特化参数、全部特化参数、特化参数+未特化参数
同时在首行需要template<>类型声明。
3)类实例化
不需要template<>声明。
只需要类名和后面的类型说明<>,和定义时不同,此时需要提供具体的类型。
4)template<>中只包含未特化、未实例化参数。
类名后的参数列表<>中可包含:未实例化参数和特化参数
cpp
// 1. 类模板
// TempClass temp(1, 2);
template <typename T, typename U>
class TempClass {
pulibc:
TempClass(T a, U b) {
cout << "basic template\n";
}
}
cpp
// 2. 类方法模板
// 2.1 非模板类的模板方法
// NonTempClass non_temp;
// non_temp.func1(123);
// non_temp.func2(456);
class NonTempClass {
public:
// 在类中直接定义模板方法
template<typename T>
void func1(T t) {
cout << "NonTempClass::func1: " << t << endl;
}
// 在类中仅声明模板方法,在类外面实现
template<typename T> void func2(T t);
}
// 在类外面实现模板方法
template<typename T>
void NonTempClass::func2(T t) {
cout << "NonTempClass::func2: " << t << endl;
}
// 2.2 模板类的模板方法
template<typename T, typename U>
class TempClass {
public:
// 在类内部实现模板方法
template<typename N>
void func1(N n) {
cout << "TempClass::func1: " << n << endl;
}
// 仅声明方法
template<typename N> void func2(N n);
}
// 在类外部定义模板方法,注意需要两个template关键字,第一个是模板类的,第二个是函数模板
template<typename T, typename U> // <----- 类模板的类型参数,第一个template关键字
template<typename N> // <----- 函数模板的类型参数,第二个template关键字
void TempClass::func2(N n) {
cout << "TempClass::func2: " << n << endl;
}
cpp
// 3. 函数模板
template<typename T, typename U>
void temp_func(T t, U u) {
cout << "temp_func: " << t << "&" << u << endl;
}
别名模板:
(1)用于为类模板定义别名。函数模板不可用,只能为类型定义别名,typedef不能用于函数。
(2)如果类模板完全特化,所有参数都明确,则不需要提供模板参数声明template<>,就不用别名模板。
(3)如果类模板偏特化,部分参数不确定,需要提供模板参数声明:template<>,此时用模板别名。
cpp
// 4. 别名模板 alias template
// 为上面定义的TempClass模板声明一个别名模板,类似模板偏特化的语法
template<typename U>
using AliasClass = TempClass<string, U>;
cpp
// 5. 变量模板
template<typename T>
constexpr T pi { T {3.1415} };
float fPi { pi<float> };
auto ldPi { pi<long double> };
- 编译器处理模板和选择性实例化
1)编译器不编译 模板,会检查语法错误。
2)在模板实例化时,用具体类型替换类型参数,生成实际代码。
3)选择性实例化:编译器不会生成所有方法的代码。
(1)所有虚方法都会生成。
(2)非虚方法,只有被调用的才会生成。
4)选择性实例化缺点:忽略错误,不能及时发现。
5)解决:显式模板实例化
cpp
Grid<int> grid; // 选择性实例化
template class Grid<int>; // 显示实例化
-
3种模板参数:类型参数、非类型参数、template template参数
这3种参数是不同维度的,尤其是类型参数和template template参数,容易混淆,在实例化时需要区分模板参数和普通类型参数,如果定义为模板参数则只能提供模板名(不能用具体类型实例化,如vector,不是vector< int >)
1)类模板和函数模板都可以使用非类型参数。
2)非类型参数支持有限类型:整型 / 枚举、指针 / 引用 / nullptr_t、auto / auto& / auto*、float、class(后2种在C++支持,有限制)
3)非类型参数需要使用constexpr,可编译时求值的数值或表达式。所有模板参数都需要在编译器确定。
4)类型参数和非类型参数都可以提供默认值。
注意:匿名模板参数也可以设置默认值,类型或非类型参数都可以。
5)模板非类型参数,其类型可以使用模板的类型参数指定的类型
6)匿名参数,在模板参数列表中定义匿名参数,这些参数在模板定义中没有用到,仅用于静态分发(通过类型选择不同的模板),或者为了应用SFINAE规则(阻止生成不符合某些规则的模板)。同时,匿名模板参数可以提供默认值。
cpp
constexpr int a = 100;
constexpr int b = 200;
// 类模板,提供模板参数默认值,constexpr
template<typename T=int, int n=a+b>
struct NonTypeArgClass {
NonTypeArgClass() {
cout << "NonTypeArg: n=" << n << endl;
}
};
// 函数模板,提供模板参数默认值
template<typename T=int, int n=2>
void NonTypeArgFunc() {
cout << "NonTypeArgFunc: n=" << n << endl;
}
NonTypeArgClass<string, 10> nonTypeArg1; // 实例化时提供全部参数
NonTypeArgClass<string> nonTypeArg2; // 只提供类型参数,非类型参数使用默认值
// NonTypeArgClass<15> nonTypeArg2; // comiplation error,如果一个参数使用非默认值,它前面的参数也不能使用默认值,和函数参数默认值一致
NonTypeArgClass<> nonTypeArg3; // 都使用默认值
NonTypeArgClass nonTypeArg4; // nonTypeArg3和nonTypeArg4完全相同
NonTypeArgFunc<string, 20>(); // 实例化时提供全部参数
NonTypeArgFunc<string>(); // 只提供类型参数,非类型参数使用默认值
// NonTypeArgFunc<25>(); // compilation error
NonTypeArgFunc(); // 和下面的调用效果完全相同
NonTypeArgFunc<>();
// NonTypeArg: n=10 // nonTypeArg1
// NonTypeArg: n=300 // nonTypeArg2,在类定义中没有用到T,结果中体现不出差别
// NonTypeArg: n=300 // nonTypeArg3
// NonTypeArg: n=300 // nonTypeArg4
// NonTypeArgFunc: n=20 // NonTypeArgFunc<string, 20>();
// NonTypeArgFunc: n=2 // NonTypeArgFunc<string>();
// NonTypeArgFunc: n=2 // NonTypeArgFunc<>();
// NonTypeArgFunc: n=2 // NonTypeArgFunc();
cpp
// 类模板,
// 第一个参数是类型参数,第二个参数是非类型参数
// 第二个参数的类型使用的类型是模板参数指定的类型,并且设置为T的默认值T()或T{ }
template<typename T, const T defaultVal = T()>
class Grid {...}
cpp
// template template 参数,模板的类型参数是另一个模板
// c++17后,class可用typename
template<..., template<parameter-list> class ParameterName, ...>
// 如下实例化,类型重复,且没有约束,可能错误写成不同类型
Grid<int, vector<optional<int>>> grid;
// 以下语法不能通过编译,vector是模板,不是类型,编译器不知道需要用int实例化vector
Grid<int, vector> grid;
// vector容器定义原型
// vector模板的类型,是除去名称vector外,剩余的部分,这些可作为模板类型参数使用
template<typename E, typename Allocator = std::allocator<E>>
class vector {...}
// 类模板定义
// 复制模板声明,把模板名称(vector)改为类型参数名,作为模板参数template<>
// c++17后,模板类型参数中的class可换为typename(class Container -> typename Container),
// 在模板类型中只使用关键字typename即可
// 模板参数默认类型为vector,不是vector<T>,Container是一个模板名,不是类型名,所以要对应vector
template<typename T,
template<typename T, typename Allocator = std::allocator<E>> class Container = std::vector>
class Grid {
private:
// 定义中使用template template参数
// Container是模板的模板类型参数,它用optional<T>进行实例化
// 模板Container用另一个类型参数T(相关类型)进行实例化
vector<Container<optional<T>>> mData;
}
// 模板方法定义
// 只需更新template<>参数列表,其他部分不变,包括方法前的类型限定,例如:
template<typename T,
template<typename E, typename Allocator = std::allocator<E>> Container = std::vector>
optional Grid<T, Container>::at(int x, int y) {...}
// 实例化,和其他参数类型模板相同,注意区分template template参数,只能提供模板名
// Grid模板第二个参数(Container)是模板类型,实例化时需要提供vector/deque(模板名)
// 不能是vector<int>或deque<int>,这些是具体的类型名
Grid<int, vector> grid1;
Grid<int, deque> grid2;
// template template参数可多层嵌套(纯属好奇心驱动的尝试)
template<typename U, // 类型参数U
// GridType的模板类型
template<typename T, template<typename E, typename Allocator = std::allocator<E>> class Container = std::vector>
class GridType // 类型参数GridType
>
class Foo {
};
cpp
// 匿名类型参数 和 匿名非类型参数,都有默认值
// 貌似提供具体的默认值没有意义,定义中用不到所以才匿名,默认值也不会被引用
// 只有使用trait提供编译时的动态值,应用SFINAE规则,控制模板在符合条件时生成,参见下面的示例。
template<typename = int, size_t = 12>
struct funcAnonymousDefault { };
// 匿名模板参数,编译期默认值
// 函数模板,比较两个值是否相等,只有两个值的类型相同时才能实例化这个模板
// typename = enable_if< is_same_v<T, U> >::type 匿名参数,默认值为enable_if<>::type
// 注意:要用enable_if::type,如果没有type则无法应用SFINAE,因为enable_if<>总是一种类型;
// 错误:typename = enable_if< is_same_v<T, U> > (末尾缺少::type)
// enable_if<arg1, arg2=void>,
// (1)如果arg1为true,则enable_if<>::type值为arg2;
// (2)如果arg1为false,则enable_if<>::type值为空,导致模板语法不合规,SFINAE阻止模板实例化
template<typename T, typename U,
typename = enable_if< is_same_v<T, U> >::type
>
bool isSameValue(T t, U u) {
return t == u;
}
int a = 3;
int b = 3;
double b = 3; // 编译错误,参见下面错误信息
cout << "is same? : " << isSameValue(a, b) << endl;
// 输出结果
// is same? : 1
// 编译错误信息:
error: no matching function for call to 'isSameValue(int&, int&)'
23 | cout << "is same? : " << isSameValue(a, b) << endl;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
note: candidate: 'template<class T, class U, class> bool isSameValue(T, U)'
15 | bool isSameValue(T t, U u) {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
note: template argument deduction/substitution failed:
error: no type named 'type' in 'struct std::enable_if<false, void>'
13 | typename = enable_if< !is_same_v<T, U> >::type
| ^~~~~~~~
- CTAD(class template argument deduction)自动推导类型参数
1)函数模板天生支持类型推导。
2)在较新版本c++中,类模板支持类型推导。需要构造函数中使用模板参数,在初始化过程中可以推断类型和值参数。
如果模板参数在初始化过程中使用不到,则无法完成类型推导。
3)通过辅助模板函数,在内部实例化模板类,支持类型自动推导。
4)特殊:unique_ptr,shared_ptr不支持类型推导,需要用make_unique() make_shared()。
原因:传入T*时,编译器无法确定是类型T还是T[ ]。
5)自定义推导规则:可以自定义推导规则,规避上面的歧义。
(1)必须定义在类定义外面。
(2)必须和类定义在同一个namespace。
(3)可选使用explicit关键字,规则和应用在构造函数上一样。
6)函数模板中,返回类型不能自动推导。可让编译器推导部分参数:
cpp
// 自定义推导规则
template<typename T>
struct DeductionRule {
explicit DeductionRule(T t) {
cout << "DeductionRule: T=" << typeid(T).name() << endl;
}
template<typename Iter>
DeductionRule(Iter iter) {}
};
// 自定义规则语法:构造函数 -> 实例化类型;
DeductionRule(int) -> DeductionRule<double>;
DeductionRule(const char*) -> DeductionRule<string>;
template<typename T>
DeductionRule(Iter) -> DeductionRule<typename iterator_traits<Iter>::value_type>;
// Output: 无自定义推导规则
// DeductionRule: T=i
// DeductionRule: T=PKc
// Output: 添加自定义推导规则后
// DeductionRule: T=d
// DeductionRule: T=NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
cpp
// 部分参数推导
template<typename R, typename T, typename S>
R func(T t, S s);
// 以下2种调用方式等效,
// 第二种省略了部分类型参数,由编译器自动推导,返回类型需明确指出
auto ret = func<long, int, int>(1, 2);
auto ret = func<long>(1, 2);
auto ret = func(1, 2); // compilation error
// 不能自动推导的类型参数,通过提供默认值,可以使调用时省略全部参数
// 本质上:部分由编译器推导,部分使用默认值,都不需要手动指定
template<typename R=long, typename T, typename S>
R func(T t, S s);
auto ret = func(1, 2);
- 模板特化:全特化、偏特化
1)类模板:支持全特化、偏特化
2)函数模板:只能全特化
(1)函数有重载机制,本质上根据不提供的类型,提供不同的实现。和模板机制类似。
(2)在函数重载解析时,函数模板不参与重载解析。
(3)函数模板特化不常用 ,可能遇到非"预期"行为。模板特化和函数重载可能误用。
3)特化语法:
(1)在模板参数列表(template<>)中,去掉特化的(类型/非类型)参数。(因为这个参数已经确定,不再需要占位符"变量")
在全特化时,只保留template<>,里面没有任何参数。
(2)类似模板实例化的语法,在类名称或者函数名称后,增加参数列表<>,列出所有 类型/非类型参数。
(3)一种"半实例化"状态,template<>告诉编译器这是一个模板,所有参数都在类定义 中类名后面列出。
偏(部分)特化 < ---- > 偏/部分 实例化
cpp
template <typename T, typename S>
struct TempClass {
TempClass(T t, S s) {
cout << "basic temp class\n";
}
};
// 类模板偏特化:用int特化第二个参数S
template <typename T>
struct TempClass<T, int> {
TempClass(T t, int s) {
cout << "specialize S\n";
}
};
// 类模板偏特化:用int特化第一个参数T
template <typename S>
struct TempClass<int, S> {
TempClass() = default;
TempClass(int t, S s) {
cout << "specialize T\n";
}
};
// 类模板全特化
template<>
struct TempClass<bool, bool> {
TempClass(bool a, bool b) {
cout << "TempClass<bool, bool>\n";
}
};
TempClass temp1 {1.0, 2.0};
TempClass temp2 {1, 2.0};
TempClass temp3 {1.0, 2};
TempClass temp4 {false, true};
// Output:
// basic template class
// TempClass<int, S>
// TempClass<T, int>
// TempClass<bool, bool>
cpp
// 非类型参数特化
// 语法和类型参数特化一样,可用于模板递归中,作为递归结束条件
template<int i>
struct TempClass2 {
TempClass2() {
cout << "TempClass2: basic template, i=" << i << endl;
}
};
template<>
struct TempClass2<10> {
TempClass2() {
cout << "TempClass2: specialized template for i=10" << endl;
}
};
TempClass2<666> temp2_1;
TempClass2<10> temp2_2;
// Output:
// TempClass2: basic template, i=666
// TempClass2: specialized template for i=10
cpp
// 函数模板全特化
template<typename T, typename S>
void TempFunc(T t, S s) {
cout << "TempFunc(T t, S s)\n";
}
template<>
void TempFunc<double, double>(double t, double s) {
cout << "TempFunc<double, double>(double t, double s)\n";
}
// 实例化函数模板并调用
TempFunc(1, 2);
TempFunc(3.0, 4);
TempFunc(5.0, 6.0);
// Output:
// TempFunc(T t, S s)
// TempFunc(T t, S s)
// TempFunc<double, double>(double t, double s)
-
模板继承
-
友元 friend:类模板的友元模板函数和友元模板类
关键:operator+后面的< T >
cpp
// 前向声明
template<typename T> class Grid;
// operator+ 声明
// 运算符+用到了模板类Grid,也要定义为模板,template<typename T>
// 其中用到的类型都为模板类型Grid<T>
template<typename T>
Grid<T> operator+ (const Grid<T>& left, const Grid<T>& right);
// 1. friend关键字声明友元。
// 2. operator+后面<T>告诉编译器此operator是模板。
// 3. 在Grid模板内部,使用Grid或Grid<T>是等效的(CLion验证)。
template<typename T>
class Grid {
public:
friend Grid<T> operator+ <T> (const Grid& left, const Grid& right);
};
// android: StrongPointer.h
template<typename T>
class sp {
// ...
private:
// 把sp自身和wp类声明为sp类的友元
// sp和wp都是模板类,friend关键字挨着class,在class前面(位于template<>之后)
template<typename Y> friend class sp;
template<typename Y> friend class wp;
}
-
函数模板返回类型和简化的函数模板语法
如果让编译器自动推导函数返回类型,可使用以下几种方式:
1)auto // 去掉const和&
2)decltype(auto) // 不会去掉const和&
3)decltype( func() )
4)auto add(T t, S s) -> decltype(t+s)
cpp
// Error,在前面decltype使用t和s时,尚未定义
template<typename T, typename S>
decltype(t + s) add(const T& t, const S& s)
{ return t+s; }
template<typename T, typename S>
auto add(const T& t, const S& s) -> decltype(t+s)
{ return t+s; }
简化的函数模板
1)所有类型都用auto(或const auto&),省略了template<typename ...>声明。
2)使用auto是编译器语法糖,效果和使用template<typename...>声明一致。
3)局限性 1:使用auto的类型每个都不同,如果需要指定多个入参为同一种类型,需要使用template的方式声明。
局限性 2: 因为auto没有类型名,在函数体内无法直接使用对应的类型,可使用decltype。
cpp
template<typename T, typename S>
decltype(auto) func(const T& t, contst S& s) {
return t + s;
}
decltype(auto) func(const auto& t, const auto& s) {
return t + s;
}
- c++20 concept
1)concept由constraints-expression(约束表达式)构成。
2)concept表达式(concept expression),应用已有concept
3)constraints-expression组成:
(1)一个简单的常量表达式,返回bool值。
(2)一种新的特殊的常量表达式,require表达式。
4)require表达式由requirement构成
5)requirement分为4种:简单、类型、复合、嵌套。
6)concept repression可使用&&或||组合使用。
cpp
// concept定义语法
template<parameter-list>
concept concept-name = constraints-expressions;
// concept表达式
// 应用已有的概念,例如:Incrementable<T>, convertible_to<bool>
// 类似于调用已定义函数:func(arg);
concept-name<argument-list>
// 约束表达式
template<typename T>
concept C = sizeof(T) == 4;
// require表达式定义语法
requires (parameter-list) { requirements; }
// requirement分为4种:
// 1. simple requirement (不以requires开头,相对嵌套requirement来说)
// 可以是任意表达式,不求值,只验证编译通过(验证语法和语义功能)
template<typename T>
concept Incremental = requires(T x) {
x++;
++x;
}
// 2. type requirement
template<typename T>
concept C = requires {
typename T::value_type; // 验证T是否有类型value_type
typename SomeTemplate<T>; // 是否可以用T实例化SomeTemplate
}
// 3. 复合 requirement compound requirement
// 验证不会抛出异常,或者返回某种类型
// noexcept, 或->type-constraint可选的,可验证某一种或同时验证
// { }是必须的,即使一个语句,也需要有。
{ expression } noexcept -> type-constraint;
template<typename T>
concept C = requires(const T x, const T y) {
{ x.swap(y) } noexcept; // noecept验证时,{}不能省略
{ x.size() } -> convertible_to(size_t); // 验证返回值时,一个语句时,{}也不可省略
{ x==y } -> convertible_to<bool>; // 不可省略{}
}
// 4. 嵌套 requirement
template<typename T>
concept C = requires (T t) {
// 这里不能省略requires,只用sizeof(T) == 4是不行的,不会实际校验,感觉requires类似assert
requires sizeof(T) == 4;
// sizeof(T) == 4; // 错误,可编译通过,逻辑不对
++t;
t++;
}
// 5. 组合概念表达式,不是-不是-不是requirement,combined CONCEPT expression
// 使用&&,||组合
template<typename T>
concept MyConcept = Incrementable<T> && Decrementable<T>;
-
模板递归
-
可变参数模板
1)< typename... Tn >表示0个或多个参数。
2)...展开其左侧的'表达式',根据参数个数重复多次,用逗号分隔。
3)...前后的空格可选。
4)可变参数没有直接的遍历方式,只能通过模板递归遍历。
cpp
template<typename... Tn>
class Temp {...}
// 可变参数函数模板
// typename... Tn,声明可变类型个数
// Tn... args,声明可变参数个数
// args...,使用可变参数
// 使用完美转发perfect forward,避免参数复制和使用字面量
// 使用确定类型参数T1和可变参数结合的方式,递归分离、解析可变参数
cpp
void handleValue(int v) { cout << "int: " << v << endl; }
void handleValue(double v) { }
void handleValue(string_view v) { }
// 递归终止case
void processValues() { }
template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args) {
handleValue(arg1);
processValues(args...);
}
template<typename T1, typename... Tn>
void processValues(T1&& arg1, Tn&&... args) {
handleValue(std::forward<T1>(arg1));
processValues(std::forward<Tn>(args)...); // 注意此处...,在每个参数上执行forward
}
// sizeof...操作符,和sizeof是不同的操作符
int count { sizeof...(args) }
使用可变参数模板实现Mixin模板类
cpp
struct Base1 {
int mValue;
Base1(int i): mValue(i) { }
void func1() {
cout << "Base1: value=" << mValue << endl;
}
};
struct Base2 {
int mValue;
Base2(int i): mValue(i) { }
void func2() {
cout << "Base2: value=" << mValue << endl;
}
};
template<typename... Cn> // 1. 声明可变参数:typename... Cn
struct Mixin: public Cn... { // 2. 继承可变参数:public Cn...
Mixin(const Cn&... classes): Cn {classes} ... {} // 3. 可变参数作为函数参数:Cn&... cls,
virtual ~Mixin() = default; // 以及初始化可变参数成员变量:Cn{classes}...
};
// 调用
Mixin<Base1, Base2> mix { Base1{123}, Base2{456}}; // 初始化方式1
auto b1 = Base1{123};
auto b2 = Base2{456};
Mixin<Base1, Base2> mix {b1, b2}; // 初始化方式2
mix.func1();
mix.func2();
// output:
// Base1: value=123
// Base2: value=456
- constexpr if
- 折叠表达式:folld expressions
- 元编程
cpp
// 模板递归,计算阶乘
template<size_t n>
struct Factorial {
constexpr static int value = n * Factorial<n -1>::value;
};
template<>
struct Factorial<0> {
constexpr static int value = 1;
};
cout << "fact: " << Factorial<1000>::value << endl;
cpp
// 模板递归,循环
template<size_t n>
struct Loop {
template<typename Func>
static void run(Func func) {
// Loop::run和func()的执行顺序,决定是正序循环还是逆序循环
Loop<n-1>::run(func);
func(n);
}
};
template<>
struct Loop<0> {
template<typename Func>
static void run(Func func) {}
};
Loop<5>::run([](int i){
cout << "run: " << i << endl;
});
- traits
- 在一个声明中多次使用template的场景
1)模板类的模板方法定义
2)模板的template template参数
cpp
// 顺序并列使用2个template关键字
template<typename ClassArgType>
template<typename MethodArgType>
void Temp<ClassArgType>::func(MethodArgType data) { ... }
// template中嵌套template
// 可嵌套多层,参见上面template template参数中的例子
template<typename T,
template<typename E, typename Allocator<E> = std::allocator<E>> Container = std::vector>
class Grid { ... }