待修改
1 定义模板
1.1 模板形参
模板参数
模板可以有两种参数, 一种是类型参数, 一种是非类型参数
这两种参数可以同时存在, 非类型参数 的类型 可以是 模板类型形参
template <
typename T, //1
T a //2
>
- 第一个参数是类型参数
T
- 第二个是非类型参数
a
, 它的类型和形参T
一致, 实际由传入实参决定
还有一种[[#1.5 模板的模板参数]]
模板的 类型参数
- 像上面的
T
, 就是类型参数 - 类型参数
T
, 可以作为 返回类型 或 参数类型 (的一部分) - 类型参数
T
, 也可用于在函数体内,变量声明, 类型转换
如何指定类型参数?
- 使用
class
或typename
来指定. - 可以指定多个类型参数, 用逗号分开
template <typename T1,typename T2>
用`typename`比`class`更好, 更直观
模板的 值参数
- 不需要使用
typename
关键字 - 需要指定值参数的类型 (该类型可以是模板参数指定的类型
T
) - 值参数传入实参应该是编译时常量
- 值参数的类型有限制
- 不能是浮点类型
- 不能是
void
类型 - 不能是数组类型, 但可以是指针类型
template<usigned int M, unsigned int N> //两个非类型参数M,N
int compare(const char (&p1)[N], const char (&p2)[M] )
//p1是常量数组 的 引用, 并且指向的数组长度为N
{
return strcmp(p1,p2);
}
值参数和 auto
在 c++11时期, 不能用 auto 指定非类型参数 的类型
但现代 c++中是允许的
template <typename T, auto N> // 现代c++可行
int func(int a[N]){
return a[0];
}
这等价于下面
template <typename T, typename V, V n>
int func(int a[n]) {
return a[0];
}
匿名的模板形参
- 模板形参可以是匿名的
- 匿名形参可以有默认实参
- 匿名形参由于没有标识符, 无法在后面使用, 但它的实参可以用于编译器检查
typename<class A, class = int>
A func(A a){return a;}
有什么妙用?
可以通过默认值是否有效性 来约束模板. 参见[[#enable_if_t]]
1.2 函数模板
基本例子
template <typename T>
int compare(const T& a, const T& b){
if (a>=b)
return 1;
else return -1;
}
其中 T
称为模板形参
调用模板函数
调用模板函数时, 可以显式地指定 模板实参, 或者让它自动推导
int a=0; int b= 1;
cout<< compare(a,b); //隐式地指定 模板实参 为 int
cout<< compare<int>(a,b); //显式地指定 模板实参 为 int
函数模板实例化
什么是实例化?
- 当重载决议结果 决定使用模板函数时, 并且模板参数类型是第一次出现, 编译器将 为这组模板参数 实例化 一个 特定版本的函数
- 也就是说, 如果之前没有创建过, 则会创造一个匹配的函数
- 这些函数也 称为模板的实例
编译器对模板的检查
当模板还未实例化时, 函数还未生成, 编译器不检查关于模板的动态语法错误.
对模板的检查有三个步骤
- 编译模板时, 关注模板语法正确性
- 使用模板时, 关注模板参数匹配性
- 实例化时.
inline 和 constexpr函数模板
inline 模板函数
template <typename T>
inline void fun (T &);
inline 模板函数的实例化 允许**内联失败**
[[§6. 函数#内联失败的诱因]]
constexpr 模板函数
条件:
- 当模板参数都能推导, 且参数在编译器可知时
- 且函数传入的实参也是常量表达式时
constexpr
模板函数实例化 可以在编译器计算
auto 和函数模板
auto 作为函数返回类型的函数模板
模板函数可以结合 [[§6. 函数#自动推导的返回类型]], 实现返回类型丰富的模板函数
注意: 推断时类型必须唯一, 多个返回语句的返回类型必须一致
例子
template <typename T1,typename T2>
auto func(T1 t1, T2 t2){
return t1+t2;
}
这个例子不能再继续展开, 因为没有实例化之前, auto 无法推导
auto 作为函数形参类型的函数模板
完全使用 auto 作为形参类型时, 可以省略 template
声明.
编译器自动为他展开为模板声明的形式.
template<> //这个可以省略
auto func(auto x,auto y){
return x+y;
}
它等价于
template<class type_parameter_0_0, class type_parameter_0_1>
auto func(type_parameter_0_0 x, type_parameter_0_1 y){
return x + y;
}
1.3 类型模板
类模板介绍
- 类型模板不是类型, 它生成的实例才是类型
- 每个不同的 类模板实例 互相独立, 他们之间没有访问特权
- 可以在类模板外部 定义成员函数, 这和类的规则一致
使用类型模板
当定义一个 实例类的 对象时, 一般需要传入所有模板实参 (除非有默认值)
在 c++17之后, 也支持在初始化时, 推导模板实参
vector<int> vec1{1,2,3};
vector vec2{1,2,3}; //ok
类模板的 普通成员函数
- 在类模板内声明的成员函数 若没有模板声明, 则是普通函数 (而非模板函数).
- 不同的实例类, 各自拥有独立的这样的普通成员函数
- 他们不是共用的, 在不同的类作用域下
- 这些成员函数 只在 对应类型被实例化时才实例化.
template<typename T>
struct myclass{
T data;
myclass(const T &t):data{t} {}
myclass():data{} {}
};
myclass<int> obj1; //实例化 类型myclass<int>, 并实例化 myclass<int>()
myclass<int> obj2{1}; //实例化 myclass(const int &t)
类模板的 模板成员函数
- 在类模板外部定义模板成员时, 需要两层
template
- 并且对于函数名, 必须使用类模板作用域运算
- 对于返回类型, 如果定义在模板中, 则也需要作用域运算. 除非是尾置的返回类型, 且前面已经说明了作用域
template<typename T>
struct myclass{
using myint = int;
template<typename V>
myint fun(const V&) const;
};
template<typename T>
template<typename V>
myclass<T>::myint myclass<T>::fun(const V &v)const{
return static_cast<myint>(v);
}
//也可以写为
template<typename T>
template<typename V>
auto myclass<T>::fun(const V &v) const -> myint {
return static_cast<myint>(v);
}
在第二种写法中省缺了 返回类型的 `myclass<T>::`
这是因为函数名字前已经有了 `myclass<T>::`, 已经能确定它的版本
类型模板的 别名
可以为某个模板类型定义别名, 此时要带上 template
可以为某个实例化 定义别名
可以为偏特化定义别名.
声明一个完全特化的类模板别名, 并不是实例化声明
例子
template<typename T, typename V>
class A{
T a{};
V b{};
};
using intA = A<int, int>; //为一个实例化 定义别名
template <typename T, typename V>
using ATV = A<T,V>; //为完整的模板 定义别名
template<typename T>
using AT= A<T,T>; //为一个偏特化模板 定义别名
int main(){
intA a1;
AT<int> a2;
ATV<int, double> a3;
}
模板类的静态成员
- 不同的实例类, 其静态成员是不同的. 它们独立的, 在不同的内存上.
- 当在外部定义静态变量时, 也是需要
template
- 在外部定义时, 可以特化该静态量
- 当使用
inline static
时, 可以在类内定义, 同时也可以在类外定义其特化版本
例子
template<typename T>
class MyClass {
inline static T value = T();
};
template<>
int MyClass<int>::value = 42; // 对于int类型进行特化
类模板的友元
类模板的友元
- 友元和类模板是独立的
- 友元可以是
- 函数, 类
- 函数模板, 类模板
- 模板函数/类 的 某个确定的 实例函数/类, 需要给定模板参数
- 模板函数/类 的某个 实例函数/类, 模板参数和类模板关联
- 注意: [[#类模板的部分特例化|部分特化]] 或它的别名 不能作为友元.
条款3,4参考下面例子
例子1
template <typename> class A; //类模板的前置声明
template <typename> class B; //类模板的前置声明
template <typename T>
bool operator==(const A<T> &, const B<T> &);
template <typename T>
class B{
friend class A<T>;
friend operator== <T>(const A<T> &, const B<T> &);
}
- 和函数声明一样, 前置声明可以不写 模板形参
- 例子中的 A 和 B 都进行了 前置声明
- 不能说 模板函数
operator==
是B
的友元. - B 的两个友元, 都是模板的
T
类型实例化A<T>
operator== <T>
举例来说, 对于类型B<int>
, 只有A<int>
和operator== <int>
才是友元
例子2: 友元是一个模板的例子
template <typename T, typename V>
class B;
template <typename V>
class B<int,V>; //部分特化
template <typename T>
void fun1(const T&);
template <typename T, typename V>
void fun2(const T&, const V&);
template <typename T>
void fun2(const T&, const int&); //这不是部分特化, 是重载
template <typename T>
class A{
template <typename V> friend class B<T,V>;
//错误, 部分特化无法作为友元
template <typename V> friend class B<int,V>;
//错误, 部分特化无法作为友元
friend class B<int,T>;
//正确, 这不是部分特化, 而是一个实例类 B<int,T>
template <typename X> friend void fun1(const T&);
//所有的fun1 都是友元
template<typename V> friend void fun2(const V&, const int&);
//ok
};
当友元是模板函数/类, 那么这些模板所有实例, 都能随意访问 类模板的 所有实例类.
当友元是 模板函数/类 的某个 实例函数/类 时, 只有对应的版本才能随意访问.
为何 部分特化的类模板 不能为友元?
当使用模板作为友元时, 必须引用一个完整的、未特化的类模板或函数模板
原因:
- 友元关系在编译时确定。编译器需要知道哪个类或函数是友元,以便授予其访问权限
- 类模板的完全特化定义是在编译时就确定的,因为它不依赖于任何模板参数的推导。
- 类模板的部分特化定义是在实例化时才生成的,因为它依赖于部分或全部模板参数的具体类型。
当我们将一个完整的模板类声明为友元时,我们实际上是将该模板所有可能的实例化都声明为友元。由于友元关系是在编译期确定的,因此这种声明是有效的。
当我们尝试将一个部分特化的类模板声明为友元时,问题就出现了。由于部分特化的定义是在实例化时才生成的,因此在编译友元声明的代码时,编译器并不知道该部分特化类的存在。
要注意分辨: 友元是否为一个模板. 下面的例子中, 友元不是模板, 但容易与模板偏特化混淆
#include <iostream>
template<typename T> class B; //前置声明
template<typename T,typename V>
class A{
public:
A(){std::cout<<"A<T,V>"<<std::endl;}
};
template<typename T>
class A<T,T>{
public:
A(){std::cout<<"A<T,T>"<<std::endl;}
void func(B<T> &b){
std::cout
<<"A<T,T>::func(B<T> &b)"
<<b.num
<<std::endl;
}
};
template<typename T>
class B{
private: int num=0;
friend class A<T,T>; //ok
//这个友元 是实例化的A<T,T>, 不是一个模板 or 模板的部分特化
//在实例化时, 它使用 特化模板进行 实例化
};
int main(){
B<int> Bob;
A<int,int> Alice;
Alice.func(Bob);
}
可以将模板的类型参数 设为友元
template <typename T>
struct myclass{
friend T; //将类型T作为友元
T data;
}
myclass<int> obj {1}; //这是可行的, 聚合初始化
在上例中, 由于 `T` 是友元, 而友元一般是类类型 或 函数, 因此此处 `T` 一般为类类型
但是**内置类型也可以作为友元**, 这种特性使得这种模板不会出错.
友元声明不引发二义性
冲突只发生在生成 实例化类 并构造对象的时刻, 只要实例类的构建没有歧义即可.
下面代码不会造成名字查找冲突
template<typename T,typename V>
class A{};
template<typename T>
class B{
friend class A<T,T>; //ok
friend class A<int,T>; //ok
};
这两个友元声明不是冲突的. 比如
- 类型
B<int>
实际上有一个友元A<int,int>
- 类型
B<double>
有两个友元A<int,doule>, A<double,double>
1.4 别名模板
别名模板介绍
- 别名模板 是一种带模板参数的, 用
using
定义的类型别名模板 - 一般来说, 用于: 使用
using
为一个类型模板 (整体或偏特化) 起一个别名- 特殊的, 只要带有模板声明, 使用 using 定义一个类型别名的, 都算别名模板
- 不能用函数模板来定义一个别名模板
句法
template <参数列表>
模板约束(可选)
using X = type_id;
typeid
是类型/类型模板 的名字X
所定义的 别名模板的名字- 可以使用
requires
进行约束
例子
这在[[#类型模板的 别名]] 有例子
template <typename T>
using A = vector<T>; //别名模板
template <typename> //匿名形参
using B = int; //这是别名模板
B<double> x =2;
A<int> vec = {1,2,3};
别名模板不能直接特化
例子
template <>
using A<double> = std::array<double,10>; //错误, 不能特化
借助类模板 特化
虽然不能直接特化, 但可以借助 类型模板的特化能力
- 可以将类型别名 写在一个类型模板里,
- 然后特化这个 类型模板
template <typename T>
struct H {
using A = vector<T>; //类型模板下的别名
};
template <>
struct H<double> { //全特化这个类型模板
using A = array<char,10>;
};
template<typename T>
using HA = H<T>::A; //HA是一个别名模板
HA
是一个别名模板, 它在类型 H
的特化帮助下, 等价有了特化性质
1.5 成员函数模板
普通类的 模板成员函数
这可视为下面的一种简化特例. 略
模板类的 模板成员函数
goto [[#类模板的 模板成员函数]]
1.6 模板的普通参数规则
模板嵌套时, 形参名字不能重用
这是因为 [[§1. 变量和基本类型, 类别#模板形参作用域 Template parameter scope]]
每个模板参数都对应一个作用域.
template <typename T>
class A{
template <typename T> //错误, 不能在模板作用域内 重用参数名
void fun();
}
分离定义和声明时, 模板参数所用记号可以不同
这和函数声明相似, 模板参数只是一个记号. 只要保证结构一致.
template <typename T>
void fun(T t); // 声明1
template <typename W>
extern void fun(W t); // 声明2
template <typename V> // 使用不同的 模板参数记号V
void fun(V t) {}; // 定义
在类模板作用域内, 可以缺省模板参数
这在构造函数中就得到体现, 如构造函数直接写为 myclass()
而不是 myclass<T>()
在成员函数体内也可以缺省 类模板参数
template<typename T>
struct myclass{
T data;
myclass(const T &t):data{t} {}
myclass<T>():data{} {}
myclass & operator++();
}
使用模板参数的 嵌套类型
当模板参数 T
是一个类类型, 且它内部 定义了[[§15. 面向对象程序设计#嵌套类简介|嵌套类型]] 或类型别名,
使用这个类型时, 必须要用 typename
告知编译器 这是一个类型
例子
template <typename T>
class A{
typename T::size_type _size; //1
A(typename T::size_type sz):_size{sz}{} //2
};
template <>
class A<std::vector<int>>{
typename std::vector<int>::size_type _size;
A(typename std::vector<int>::size_type sz):_size{sz}{}
};
- 在定义成员
_size
时, 它的类型是T
的内部类型size_type
, 因此要用作用域运算- 此时在它内部, 和内部类型
size_type
有关的模板参数T
不能缺省 - 构造函数名可以缺省
<T>
- 此时在它内部, 和内部类型
- 在一个实例化中, 将
T
作为vector<int>
类型, 其中的size_type
是在vector<int>
内部定义的类型别名
为何要告知编译器?
- 因为在编译模板时, 在实例化之前, 在编译器看来 `T::size_type` 可能是 `T` 的一个数据成员/函数成员/类成员 的一种.
- 编译器*默认将其解释为 数据成员/函数成员*, 而不是类型
- 在C++20中, `typename`可以缺省, 它会被隐式地补全.
template <typename T>
class A{
T::size_type * _size;
//当低于c++20 编译器认为 这是将 T::size_type 与 _size 相乘
};
模板参数的默认实参
- 模板参数可以有默认实参
- 在多个声明中, 可以增加新的默认实参.
- 在新的声明中, 不能写已有的实参, 否则视为重定义
- 增加实参后, 保证所有的实参位置连续, 且都在末尾.
template<typename T, typename V, const int M = 10>
class A{
int a[M];
T t;
V v;
};
template<typename T, typename V = double , const int M =10>
class A; //error, const int M =10 是重定义
template<typename T = int, typename V, const int M>
class A; //error, T和M位置不连续
template<typename T, typename V = double , const int M>
class A; //ok
- 这和函数的默认实参规则类似
- 模板实参在std中很常见, 比如`compare`函数有一个默认实参的偏序
为类型模板 传入 模板实参
- 当使用类型模板 的 实例化类型时, 一般需要传入所有 模板实参.
- 如果因为有了模板默认实参, 而无需额外模板参数时, 最好也带上空的
<>
- 在 c++17之后, 编译器可以推导类型模板的 模板实参, 但仍然建议写上
template<typename T = int>
class A{};
A<> a1; //ok
A a2; //error, 但编译器能推断其为 A<>类型
尽管第二种写法可能不报错, 但应该带上它, 表示这是模板
例子
template<typename T = int>
struct A{
T data;
A(T data): data(data){}; //删除该行后, 写法2可能报错, 不删除这行, 则不报错. 写法1总是可行的, 是为什么?
};
int main(){
A<> a1 {1}; //写法1
A a2{1}; //写法2
std::cout<<a1.data<<a2.data<<std::endl;
}
- 当删除 那个构造函数后, 类型称为聚合类型.
- 写法1是可行的, 这是聚合类的初始化
- 写法2 可能不可行 (c++20之前), 在进行聚合初始化前, 必须明确类型.
- 在 c++20之后都是可行的.
- 当没有删除那行时, 写法2 可以先通过构造函数推导出模板实参
T
, 这样就确定类型了.- 就像在定义
std::array
对象时, 如果进行列表初始化, 可以不写模板实参
- 就像在定义
1.7 模板的模板参数
模板模板形参的声明
模板模板形参声明语法如下
template <typename, typename > class T //1
template <typename...> class T //2
- 模板模板形参
T
是一个类型模板, 它有两个模板形参 - 模板形参
T
是类型模板, 它是可变参数的类型模板.
声明语句必须嵌入到 模板声明中, 比如:
template<
template<typename,typename> class T,
template<typename ...> class U,
typename V
> struct myclass{};
在c++17之前, 必须使用 `class` 声明 模板模板形参
模板模板形参的实参, 类型别名
实参必须是类型模板(class template), 或类型别名 (type alias)
可以为类型模板 起一个类型别名, 使用 using
的模板用法 [[#类型模板的 别名]]
然后将这个 alias 作为模板模板实参
template <typename T>
using myvec = std::vector<T>; //myvec是 vector的一个部分特化版本的别名
// 此时 使用 myvec<T> 就等价于 vector<T>
例子
template<
template<typename> class T, //第一个模板参数 T
typename V
>
struct B;
template <typename T>
using vec = std::vector<T>; //vec是别名模板
int main(){
B<vec,int> b1; //ok
B<vector<int>, int> b2; //error , vector<int>是一个具体的类型
B<std::vector,int> b3; //error, vector本身可以接收两个参数
}
- 这个例子中, 最后一个提示 "Template template argument has different template parameters than its corresponding template template parameter"
- 模板模板实参 有 不同的模板形参, 和对应的模板模板形参
T
比较起来
- 模板模板实参 有 不同的模板形参, 和对应的模板模板形参
- 这是因为
std::vector
模板有两个形参, 一个是元素类型, 另一个是分配器类型. - 而在声明中
template<typename> class T
, 只有一个typname
, 这意味着T
只有一个模板形参
在 c++17之前, 必须使用 `class` 而非 `typename` 来声明 模板的模板形参
1.8 控制模板的实例化
实例化可能重复
- 相同的模板实例, 可能出现在多个文件中.
- 若这些文件都独立编译, 则每个文件都生成一个实例化, 这样可能重复生成.
比如 vector<int>
出现在两个文件中, 且这两个文件独立编译, 最后归并. 那么有两个 vector<int>
实例.
实例化声明
可以显式定义一个实例化, 句法如下
template 显式实例化声明语句;
在 实例化声明语句
中, 必须指定所有模板参数(除默认实参外), 这样可以显式地实例化 某个函数/类型
在其他文件如果要使用该实例化, 则 只要声明即可. 仅声明语句如下:
extern template 显式实例化声明语句;
例子
//头文件:my_template. h
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// 源文件:my_template.cpp
#include "my_template.h"
template int max<int>(int a, int b); // 显式实例化定义 max<int>
//其他源文件:another_file.cpp
#include "my_template.h"
extern template int max<int>(int a, int b);
//仅声明, 告诉编译器, 在其他文件 已经实例化了
int main() {
int x = max<int>(3, 5); // 使用实例化
}
- 在第二个文件中, 使用显式实例化定义
- 第三个文件, 使用显式实例化声明.
extern
告诉编译器, 已在其他地方定义了该实例化.
注意声明语句和定义的区别
声明中只要使用 `template`, 实例化定义中需要 `template<>`
2 模板的效率和灵活性, 以智能指针举例
为智能指针自定义删除操作
[[§12. 动态内存 智能指针#1.5 为 shared_ptr 自定义 释放操作]]
unique_ptr
和 shared_ptr
都可以自定义删除操作, 但非常不同
- shared_ptr 中, 自定义删除操作是作为"成员"
- unique_ptr 中, 删除器的类型 必须 填入
unique_ptr
的模板实参中.
例子
void mydelete(int *p) { delete p; std::cout<<"haha";}
int main() {
shared_ptr<int> sp(new int(1), mydelete);
unique_ptr<int, decltype(*mydelete)> up(new int(1), mydelete);
}
3 模板实参推断
显式指定部分模板实参
在调用模板时, 可以只指定前面部分实参类型.
template <typename T, typename V>
auto func(T a, V b) {
return a + b;
}
int main() {
int a = 1;
double b = 1;
auto x = func<int>(a,b); // ok
}
尾置 decltype 的返回类型
在 auto
用于返回类型推导之前 [[§6. 函数#auto 作为返回类型]], 可以用 decltype 来解析返回类型
template <typename T, typename V>
auto func(T t, V v) -> decltype(t + v) {
return t + v;
}
现代 c++则可以直接 auto 即可
模板实参转换
- 当已经实例化/特化了一个模板后, 只有有限的类型能匹配该实例化版本, 并可能发生隐式转换.
- 对于不能匹配的类型, 将生成一个新的模板的实例.
- 可行的转换如下:
const
转换: 底层非 const 的引用/指针 传递给 const 引用/指针形参- 数组或函数指针 的转换: 数组实参可以转换为指向首部的指针; 函数名字 可以转换为 函数指针
例子
#include <iostream>
using namespace std;
template <typename T> void func(T t) { cout << 0 << std::endl; }
template <> void func<const int &>(const int &t) { cout << 1 << endl; }
template <> void func<int *>(int *t) { cout << 2 << endl; }
template <> void func<void (*)(int)>(void (*t)(int)) { cout << 3 << endl; }
void fo(int n) {}
int main() {
int a{1};
int &x = a;
int b[2]{1, 2};
func(x); // 调用0
func<const int &>(x); // 调用1
func(b); // 调用2
func(fo); // 调用3
}
在该例中, 虽然参数不能完全匹配 已经显式实例化的 三个函数, 但能进行转换.
因此不会再生产额外的实例化.
#include <iostream>
template<typename T>
void func(T t){
std::cout << "T" << std::endl;
}
template<>
void func<const int &>(const int & t){
std::cout << "const int &" << std::endl;
}
void fo(int n){}
int main(){
int a {1};
int &x = a;
func(x);
}
利用 type_traits 进行类型转换
请参考 [[#6.2 type traits]]
类型实参无法推断的情况
此时必须显式写出类型实参
例子1 返回类型是模板类型, 无法推断.
template <typename T1 ,typename T2>
T1 func(T2 t){
return static_cast<T1>(t);
}
double x = func<double,int>(3);
例子2 当存在推断冲突时
template <typename T>
T func(T t1, T t2){
return t1+t2;
}
auto x = func(1,1.2) ; //错误无法推断
auto x = func<int> (1,1.2) ;//ok, 发生隐式转换
4 可变参数模板 和 参数转发
4.1 参数包
参数包的概念
可变数目的参数, 称为参数包
有两类参数包
- 模板参数包: 参数包作为 模板参数
- 函数参数包: 参数包作为 函数形参
参数包的写法示例
template <typename... Args>
void foo(const Args&... args);
template <typename... Args>
void foo(const Args...& args); //错误, 原因见下面note
typename... Args
: 声明了一个可变模板参数包 Args
const Args& ... args
: 表示 args
是一个函数形参包
在声明函数参数包时,正确的语法是将...放在类型名(包含复合类型)的右边,然后是参数名
具体参见[[#形参包语法]]
可变参数函数模板 递归的例子
template <typename T>
ostream& print(ostream &os, const T &t){
return os << t ;
}
template <typename T, typename ...Args>
ostream& print(ostream &os, const T &t, const Args&... rest){
os << t << ", ";
return print(os, rest...); //将rest展开, 再传入函数print
}
int main(){
print(std::cout, 1, 2, 3, 4, 5);
cout << endl;
}
- 第一个是非可变参数 函数模板作为递归基
- 这两个函数声明的顺序不能交换. 否则出错.
- 虽然模板实例化发生在
main
函数中, 并且此时ostream& print(ostream &os, const T &t)
已经被编译了. 然而在实例化 该模板时, 非模板版本函数 是不可见的, 因为它没有前向声明 - 这导致无法达到递归基函数
ostream& print(ostream &os, const T &t)
, 从而引发无限递归和匹配失败
- 虽然模板实例化发生在
形参包语法
模板形参包语法
[类型] ... [包名(可选)]
template<int... args> // [类型] ... [包名(可选)]
void func() {
ifunc(args...);
}
[typename|class] ... [包名(可选)]
template<class... Args> // [typename|class] ... [包名(可选)]
void func(Args... args) {
}
[概念约束] ... [包名(可选)]
(C++20 起)
template<std::integral... Ints> // 类型约束 ... 包名(可选)
void sum(Ints... values) {}
template <形参列表> class ... [包名(可选)]
(C++17 前)
这属于模板模板形参的写法
template<template<typename> class... Templates> // template < 形参列表 > class ... 包名(可选)
class Container {};
template <形参列表> typename|class ... [包名(可选)]
(C++17 起)
这属于模板模板形参的写法
c++17开始, 模板模板形参可以用 typename 声明
template<template<typename> typename... Templates>
// template < 形参列表 > typename|class ... 包名(可选)
class Container{};
函数参数包语法
[包名] ... [包形参名(可选)]
template<typename... Args>
void func(const Args&... args) { // 包名 ... 包形参名(可选)
}
形参包展开语法
[模式] ...
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n'; //这是折叠表达式
}
形参包的参数数目可以为0
参数包展开 中的 模式
模式(pattern) 是指在参数包展开 过程中, 应用到每个元素的一种模板表达式或格式。
在进行包展开时,模式与包中的每个元素结合,生成相应的代码片段。
模式举例1
template <typename... T>
int baz(T... t) { return 0; }
template <typename... Args>
void foo(Args... args) {}
template <typename... Args>
class bar {
public:
bar(Args... args) {
foo(baz(&args...) + args...); //*
}
};
int main(){
bar<int,double> b(1,1.5);
}
有两处使用了包展开语法,
- 第一个 模式是
&args
- 第二个模式是
baz(&args...) + args
展开的效果为
foo(baz(1,1.5) + 1, baz(1,1.5)+1.5);
int f1(int a,int b) {return 1;}
double f2(int a,int b) {return 1;}
template <class ...Args>
void foo(Args (*...args)(int, int)) {
int tmp[] = {(std::cout << args(7, 11) << std::endl, 0) ...};
}
int main(){
foo(f1,f2);
}
在函数中, 虽然定义了一个数组, 但这个数组只用于服务 包展开语法.
Args (*... args)(int, int)
是函数的 形参包(std::cout<<args(7,11)<<std::endl,0)...
中, 模式为(std::cout<<args(7,11)<<std::endl,0)
最后执行相当于
int tmp[] = {(std::cout<<f1(7,11)<<std::endl,0), (std::cout<<f2(7,11)<<std::endl,0)};
- tmp 将是一个二元数组, 且元素为
{0,0}
(std::cout<<f1(7,11)<<std::endl,0)
是逗号表达式, 该表达式将从左往右执行, 并返回右边的值, 因此每个这个都是0
包展开的应用场景
在很多地方都允许 使用包展开
- 表达式列表
- 初始化列表
- 基类描述
- 成员初始化列表
- 函数形参列表
- 动态异常列表
- lambda 表达式捕获列表
- sizeof...() 运算
- 内存对齐运算符 alignment operator
- 属性列表
例子
#include <iostream>
#include <initializer_list>
#include <tuple>
#include <utility> // for std::forward
#include <type_traits> // for std::aligned_storage
// 表达式列表
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args);
}
// 初始化列表
template<typename... Args>
void initList(Args... args) {
std::tuple t = { args... };
}
// 基类描述
template<typename... Bases>
struct Derived : Bases... {
Derived(Bases&... bases) : Bases(bases)... {}
};
// 成员初始化列表
struct MemberInit {
std::tuple x;
std::tuple y;
template<typename... Args>
MemberInit(Args... args) : x(args...), y(args...) {}
};
// 函数形参列表
template<typename... Args>
void func(Args... args) {
std::cout << sizeof...(args) << '\n';
}
// 动态异常列表 (C++17后已弃用,但这里为示例)
void dynamicException(...) throw(int, double) {}
try {
dynamicException();
} catch (int) {
std::cout << "Caught int\n";
} catch (double) {
std::cout << "Caught double\n";
}
// lambda 表达式捕获列表
int a = 1, b = 2, c = 3;
auto lambda = [=]() {
return (a + b + c);
};
// 内存对齐运算符 alignment operator
template<typename... Args>
struct Aligned {
alignas(Args...) char data[sizeof...(Args)];
};
Aligned<int, double, char> alignedObj;
std::cout << alignof(decltype(alignedObj)) << '\n'; // 输出: 8 (或其他值,取决于平台)
// 属性列表
template<typename... Args>
void attr([[maybe_unused]] Args... args) [[deprecated]] {
// 函数体
}
attr(1, 2, 3); // 调用被标记为deprecated的函数,可能会有警告
sizeof... 运算符
获取参数包的参数数目
可获取类型参数的数目, 或者函数参数的数目
template <typename T, typename... Args>
void foo(T & t,const Args &... args){
auto num1 = sizeof...(Args);//获取类型参数数目
auto num2 = sizeof...(args);//获取函数参数数目
auto num3 = sizeof...(T); //错误,不能获取非参数包的 类型参数数目
}
折叠表达式
折叠表达式出现之前, 要优雅地使用包展开是麻烦的, 比如 [[#参数包展开 中的 模式]]中, 利用数组和括号表达式 展开的技巧性太强.
- 折叠表达式(fold expression),它是从 C++17 引入的一种语法
- 用于简化和美化处理参数包的代码
- 折叠表达式有四种
- 注意折叠表达式必须加括号
(... op [包名/模式] ) //一元左折叠
([包名/模式] op ... ) //一元右折叠
([初值] op ... op [包名/模式]) //二元左折叠
([包名/模式] op ... op [初值]) //二元右折叠
op
是运算符- 折叠表达式中的
...
表示将参数包[包名/模式]
展开, 并传递给运算符
注意, 逗号运算符也是操作符, 也可以进行折叠表达式
例子
template<typename... Args>
void printer(Args&&... args)
{
(std::cout << ... << args) << '\n'; //<<是运算符号
}
template<typename... Ts>
void print_limits()
{
((std::cout << +std::numeric_limits<Ts>::max() << ' '), ...)
//模式为 (std::cout << +std::numeric_limits<Ts>::max() << ' ')
// 折叠表达式 依赖 逗号运算符
}
template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args)
{
static_assert((std::is_constructible_v<T, Args&&> && ...));
// 模式为std::is_constructible_v<T, Args&&>
// 折叠表达式 依赖 &&运算符
(v.push_back(std::forward<Args>(args)), ...);
// 模式为 v.push_back(std::forward<Args>(args))
// 折叠表达式 依赖 逗号运算符
}
// Using an integer sequence to execute an expression
// N times by folding a lambda over operator,
template<class T, std::size_t... dummy_pack>
constexpr T bswap_impl(T i, std::index_sequence<dummy_pack...>)
{
T low_byte_mask = static_cast<unsigned char>(-1);
T ret{};
([&]
{
(void)dummy_pack;
ret <<= CHAR_BIT;
ret |= i & low_byte_mask;
i >>= CHAR_BIT;
}(), ...);
// 模式为 lambda 表达式, 它和 形参包 dummy_pack 有关
// 折叠表达式 依赖 逗号运算符
return ret;
}
constexpr auto bswap(std::unsigned_integral auto i)
{
return bswap_impl(i, std::make_index_sequence<sizeof(i)>{});
}
折叠表达式如何进行
假设 $args$ 包含了元素 $arg0,arg1,...argN$
//左折叠
(args + … )折叠为(arg0 + (arg1 + … (argN-1 + argN)))
//右折叠
(… + args )折叠为((((arg0 + arg1) + arg2) + …) + argN))
//左折叠
(args + … + init)折叠为(arg0 + (arg1 + …(argN-1 + (argN + init)))
//右折叠
(init + … + args)折叠为(((((init + arg0) + arg1) + arg2) + …) + argN)
例子1
template<typename... Args>
auto sum(Args ...args){
return (args + ...); // 一元右折叠
}
例子2
template<typename... Args>
void print(Args ...args){
(std::cout<<...<<args); //二元右折叠, 初始为 std::cout
// 非折叠的写法
int tmp[] = {
(std::cout<<args,0)...};
}
- 这是二元右折叠的例子, 比起用数组的方法更加直观
- 初值是
std::cout
对象
参数包为空时, 如何折叠
template<typename... Args>
auto sum(Args ...args){
return (args + ...); // 一元右折叠
}
以这个为例子:
- 一元折叠表达式对空参数包展开时, 将无法推导返回类型
- 二元折叠表达式, 可以指定初始值, 则避免该问题
return (args + … + 0);
但一些情况下, 一元折叠表达式 对空参数包仍然可以推导:
- 只有
&&
||
,
运算符 - 对于空参数包
arg && ...
结果为true
arg || ...
结果为false
arg , ...
结果为void{}
在 using 声明中使用包展开
当使用 using 引入某个名字时, 可以使用包展开语法
template<typename... Args>
class A: public Base<Args>...{ //继承中的包展开
using Base<Args>... ; //using 包展开
//using Set = Base<Args>...; //错误, 不存在这样的语法
}
这相当于将所有继承的 Base<Args>
的构造函数都继承
注意不能将包展开用于 using类型别名
int tmp[] = {(using A = Args,0)...}; //错误的
4.2 参数转发
什么是完美转发?
- 当有嵌套的函数时, 需要将传入的参数 传递给内部函数时, 称为参数转发
- 参数转发途中, 值类别和 cv 限定可能发生改变.
- 完美转发可以保证 值的类别, 以及 cv 限定符
- 只能保证值类别, 即左值或右值
- 当传入左值表达式时, 转发为左值引用
- 当传入右值表达式时, 转发为右值引用
万能引用和 static_cast 用于完美转发
[[§4. 表达式概念和一些特殊表达式#万能引用]]
template <typename T> void show_type(T &t) {
std::cout<<"左值"<<std::endl;
}
template <typename T> void show_type(T &&t) {
std::cout<<"右值"<<std::endl;
}
template <typename T> void perfect_forwarding(T &&t) {
show_type(static_cast<T&&>(t));
}
int main() {
int t = 1;
perfect_forwarding(t); //传入左值int, 转发时为 int&
perfect_forwarding(1); //传入右值, 转发时为 int&&
}
perfect_forwarding(t)
,t
是左值, 因此模板推导T
为int &
,- 从而
T&&
折叠为int &
, static_cast<T&&>
等价于static_cast<int &>
, 将其转换为int&
- 然后调用左值引用版本的
show_type
perfect_forwarding(1)
1
是右值, 因此模板推导T
为int
,- 从而
T&&
为int &&
, static_cast<T&&>
等价于static_cast<int &&>
, 将其转换为int&&
- 然后调用右值引用版本的
show_type
std:: forward 用于完美转发
函数模板原型
template <typename _Tp>
constexpr typename std::remove_reference<_Tp>::type && //返回类型右值引用
move(_Tp &&__t) noexcept { //形参是万能引用
return static_cast<typename std::remove_reference<_Tp>::type &&>(__t);
}
- 当传入的为左值
int
类型时, 则_Tp
被推导为int&
, 形参类型为int & &&
并折叠为int &
- 当传入的为左值
int
类型时, 则_Tp
被推导为int
, 那么形参类型为int &&
std::forward
实际上也是使用了 static_cast
, 但它更方便
template <typename T> void show_type(T &t) {
std::cout<<"左值"<<std::endl;
}
template <typename T> void show_type(T &&t) {
std::cout<<"右值"<<std::endl;
}
template <typename T> void perfect_forwarding(T &&t) {
show_type(std::forward<T>(t));
}
int main() {
int t = 1;
perfect_forwarding(t);
perfect_forwarding(1);
}
decltype(auto) 用于参数转发
参数包转发
template<typename... Args>
void func(Args&&... args) {
other_func(std::forward<Args>(args)...);
}
5 模板特例化
5.1 函数模板特例化
定义函数模板特例化
- 函数模板只有完全特化, 必须使用
template<>
, 这告诉编译器, 我们将手动提供所有模板实参 - 模板完全特例化并不是 模板重载, 它只是接替了编译器的工作.
- 特例化不影响函数匹配优先性
- 独立的非模板函数 会影响 函数匹配. 在参数都可匹配的情况下, 更优先使用非模板函数
template <typename T>
bool compare(const T&, const T&); //模板
template <>
bool compare(const int &, const int &); //完全特例化
bool compare(const double &, const double &);//重载
函数模板没有部分特例化
它可以写成类似 类型的部分特化的形式, 但这并不是部分特化. 而是函数模板重载
#include <iostream>
template<typename T, typename U>
void f(T*, U) { std::puts("3"); }
template<typename T, typename U>
void f(T, U) { std::puts("1"); }
template<typename U>
void f(int, U) { std::puts("2"); }
int main() {
f(1, 1);
int a = 1;
f(&a, 1);
}
- 这三个模板之间不构成特化关系 (可以这么理解, 如果构成偏特化关系的话, 第二个模板应该写在最前面)
- 事实上这是函数模板重载
函数重载和模板函数特例化的区别
当对于一个可见的模板函数声明而言
另一同名函数
-
若没有带
template <>
, 则是函数重载 -
若带有
template <>
, 且不是完全特化的函数模板, 则是模板函数重载 -
重载函数模板不需要其他同名模板可见
-
而特例化必须要求原母模板可见
template <typename T,typename U>
void fun(T &, U &){}
void fun(int &, double &){} //重载
template <typename V>
void fun(V &){} //重载
template <typename T, typename U, typename V>
void fun(T &, U &,V &){} //重载
函数重载时, 其参数列表必须不同, 否则报错.
但是两个模板函数有重载问题时, 编译器不报错, 这是因为函数如果没有实例化, 函数还没有生成, 无法检查.
template <typename T,typename U>
void fun(T &, U &){}
template <typename T, typename U>
int fun(T &, U &){} //错误, 不能重载, 但此时编译器不报错
int main(){
int a=1,b=1;
fun(a,b); //此时报错, 此时模板函数必须实例化, 从而发现冲突
}
建议使用重载, 而非模板全特化
这是因为模板特化 不参与重载解析,
可能会被另一个不完全匹配的重载版本替代, 这可能二义性
例子
template<typename T>
void fun(const T& t){cout<< "模板";}
template<>
void fun(const int& t){cout<< "模板特化int";}
void fun(const double& t){cout<< "重载double";}
int main(){
fun(1); // 调用 模板特化int
fun(1.0); // 调用 重载double
}
该例中, 依然可以调用最匹配的版本.
大部分情况下, 还是不用担心 模板特化和重载的选择问题.
特例化时, 原模板声明必须可见
- 当 特例化/偏特化 一个模板时, 它的原模板声明必须在作用域中
- 这对于 类型模板/函数模板/别名模板 都一样
特例化声明 的一些规则
- 特例化声明不可见, 而原模板可见时, 特例化可能失效
- 编译器没有看见特例化定义, 将使用原模板 实例化
- 从而绕过了用户在其他文件/作用域中声明的 特例化版本
- 特例化声明 必须 出现在 程序对模板实例化 之前
- 如果程序在没有遇到特例化声明时, 就已经使用了模板, 那么编译器将生成一个 模板实例化的函数.
- 这个实例化的函数 和 特例化的函数 如果参数都相同, 则发生冲突, 名字查找将发生歧义.
模板和特例化版本, 应放一个头文件中, 模板声明在前, 然后是特例化声明
5.2 类模板特例化
类模板特例化的介绍
特化规则
- 类模板的 特例化版本, 和原模板必须在 同一命名空间中
- 类模板 特例化时, 可以只特化其中某个成员函数
- 这种情况不能增删成员
完全特化和偏特化
- 完全特化: 指定所有的模板参数
- 偏特化: 只特化部分参数,
特化部分成员函数
对于一个模板类型, 可以特化它的成员函数.
当特化成员函数时, 只能完全特化, 而不能偏特化, 除非它在偏特化的类型中定义.
template <typename T> struct A {
void func() {
std::cout << "T";
}
};
//只特化某个成员
template <> void A<int>::func() {
std::cout << "int";
}
不能特化部分成员变量
在特化成员函数时, 不必"重新定义"类型.
而如果要特化某个成员变量的初始值或者类型时, 则必须重新写出它的定义, 一旦写出类定义, 那么不能复用母模板的其他代码.
template<typename T>
struct A{
int a = 0;
vector<T> v{};
void f(){std::cout<<"AT";}
};
template<>
struct A<int>{ //这里重新定义了 A<int>, 因此它没有成员a, f()
vector<double> v;
};
int main(){
A<int> Aint;
Aint.f(); //出错 没有这个成员
}
特化时可以修改继承关系
在特化时, 如果想不修改继承关系, 不能缺省, 否则视为没有继承
例子
struct B {
int b = 0;
};
template <typename T> class D : B{
void func() { ++b; }
};
template <> class D<int> { //妄图缺省继承
void func() { ++b; } //错误, 因为没有继承B, 所有没有这个成员
};
可以修改继承关系
例子
#include <iostream>
class B1 {
public:
B1() { std::cout << "B1" << std::endl; }
};
class B2 {
public:
B2() { std::cout << "B2" << std::endl; }
};
template <typename T>
class D : public B1 {
public:
D():B1(){}
};
template <>
class D<int> : public B2 { //特化修改了继承关系
public: D() : B2() {}
};
int main() {
D<double> d1;
D<int> d2;
}
特化时可以修改友元关系
#include <concepts>
template <typename T> class A {
private:
int a = 0;
friend void func1(A<T> &A);
};
template <> class A<int> {
private:
int a = 0;
friend void func2(A<int> &A);
};
template <typename T>
concept NotInt = !std::same_as<T, int>;
template <NotInt T>
void func1(A<T> &A) { ++A.a; };
void func2(A<int> &A) { ++A.a; };
int main() {
A<double> A1;
A<int> A2;
func1(A1); //ok
func2(A1); //error
func1(A2); //ok
func2(A2); //error
}
- 特化的类型
A<int>
修改了友元,- 它没有
template <NotInt T>void func1 (A<T> &A)
作为友元 - 只
func2
作为友元
- 它没有
- 这里用到了概念 concept 约束模板类型, 否则会报错.
- 因为如果没有约束类型,
func1<int>
需要访问A<int>
的 private 成员. - 加上约束后, 不会生成
func1<int>
函数
- 因为如果没有约束类型,
类模板的偏特化
类模板的部分特例化 (偏特化)
- 部分特化 是类模板独有的概念. 模板函数没有部分特化 (而是重载), 别名模板不能特化
- 部分特化中 不必提供所有模板实参
- 如果类模板 有多个 模板参数, 可以只提供一部分参数
- 对于每个模板参数, 可以只提供其部分特性(比如 const, 指针, 引用等)
- 类模板的部分特例化, 本身又是一个类模板
- 可以进一步对它 进行 部分特例化 / 完全特例化.
例子1: 引用类型特例化**
template<typename T>
class myclass{}; //类模板
template<typename T>
class myclass<T&>{}; //部分特例化, 对引用类型特例化
template<typename T>
class myclass<T&&>{}; //部分特例化, 对右值引用类型特例化
template<typename T>
class myclass<T*>{};
template<typename T>
class myclass<T**>{};
int main(){
myclass<int> a;
myclass<int&> b;
myclass<int&&> c;
myclass<int*> d;
myclass<int**> e;
return 0;
}
myclass<T&&>
不是myclass<T&>
的部分特化, 它们是不同的类型myclass<T**>
不是myclass<T*>
的部分特化
例子2: 部分模板参数特例化
template<typename T,typename V>
class A;
template<typename V>
class A<int,V>;
template<typename T>
class A<T,T>; //正确 但和上面那个可能冲突,导致歧义性
int main()
{
A<int,int> a; //错误, 有二义性
}
当使用 A<int,int>
, 有两个可以匹配的 特例化模板. 这导致二义性
函数模板没有 部分特例化
偏特化和名字查找规则
偏特化的模板并不参与名字查找阶段, 名字查找只查找主模板.
当确定使用一个主模板后, 才会考虑这个主模板的部分特化
偏特化排序
- 当名字查找规则决定使用某个主模板后, 并且该主模板有多个可见的部分特化.
- 需要对匹配程度排序. 将使用约束最多, 最专门化的的特化版本
- 如果存在多个偏特化都是最佳匹配, 则无法编译.
特例化 类成员
6 模板的参数约束 concept
6.1 概念 concept
concepts 介绍
作用
概念是对 C++核心语言特性中模板功能的扩展
它在编译时进行评估, 对类模板、函数模板以及类模板的成员函数进行约束
它定义在 concept
头文件中.
一个概念例子 std:: integral
在头文件 concept
定义如下
template<typename T>
concept integral = std::is_integral_v<T>;
这定义了一种约束条件(概念) integral
这个约束概念可用在模板形参声明中, 用于限定类型范围.
template <integral T> //在模板形参声明中使用
auto func(T t) {return t};
func<int>(1); //编译成功
func<double>(1.5); //编译失败, double不是整型
** SFINAE (Substitution Failure Is Not An Error)**原则
概念的实现还依赖于 SFINAE 原则。SFINAE 指的是,如果在模板实例化过程中出现类型替换错误,编译器不会直接报错,而是尝试其他可行的模板实例化。
在概念的上下文中,如果传递给概念的类型不满足约束,则会导致模板实例化失败。但由于 SFINAE 原则,编译器不会报错,而是将该实例化视为不可行,并尝试其他可行的实例化。
如果所有可用的模板都无法用这些实参, 才会报错
concepts 语法
concept 是有名字标识符的一组 requirements 的组合. 句法如下
template < template-parameter-list >
concept concept-name attr (optional) = constraint-expression;
- 属性
attr
是可选的 - [[#约束表达式 constraint expression]] 中不能递归调用
concept-name
- concept 不能被显示实例化, 特化
- concept 不能被进一步的约束.
不能被进一步约束的例子
template<typename T>
concept BaseConcept = requires(T t) {
{ t + t } -> std::same_as<T>;
};
template<BaseConcept T> //这里已经用 BaseConcept 约束了 T
concept DerivedConcept = requires(T t) { //尝试进一步约束T ,不可行
{ t * t } -> std::same_as<T>;
};
concept 的作用
concept 可用于
- 用在模板的参数声明中, 约束类型实参
- 占位符类型说明符
- 在 [[#Compound requirements]] 中使用.
例子
template<integral T> //将 Integral 用于模板的参数声明
void func(T t);
integral auto func(integral auto a, char b) {
//用于约束 auto的推导, 说明类型必须是整型, 因此类型可以是 int char等
return a + b;
}
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; //在符合需求中使用concept
};
约束表达式 constraint expression
它是定义一个 concept 的核心部分. 它可以是
- 纯右值 常量 bool 表达式, 在编译器可求值
- [[#requires 表达式]]
- 单独的 concept
- 以上三种的复合形式, 通过逻辑运算符结合.
template<typename T>
concept integral = std::is_integral_v<T>; //std::is_integral_v<T>;是bool表达式
template<typename T>
concept All = true; //总成立的concept, true是常量表达式
template<typename T>
concept C = requires { //使用require表达式
std::is_integral_v<T>;
};
template<typename T>
concept C = integral; //使用其他概念
template<typename T>
concept C = std::integral<T> || std::is_class_v<T>; //复合形式
约束表达式也可用于 [[#requires 子句]]
6.2 使用 std:: enable_if 约束模板
类型谓词和类型谓词结果
- Type Predicate 类型谓词一般是一种类型模板
- 它接受一个或多个类型作为参数, 并包含一个名为
value
的static constexpr bool
成员变量, 该变量存储着类型检查的结果 - C++17 引入了一种更简洁的方式来使用类型谓词,即使用
_v
后缀,可以直接获取类型谓词的结果- 例如
std::is_integral_v<T>
获取了类型谓词is_integral<T>
的结果
- 例如
is_integral_v 和 is_integral
is_integral_v
是[[#变量模板简介|变量模板]], 它
- 是类型模板的静态成员
is_integral<_Tp>::value
- 当
T
为整数时, 其值为true
, 否则为false
- 它还是[[§6. 函数#inline 说明符|内联变量]]
- 其定义如下
template <typename _Tp>
inline constexpr bool is_integral_v = is_integral<_Tp>::value;
is_integral
是类型模板, 它继承了一个类型别名
其定义如下
template <typename _Tp>
struct is_integral : public __is_integral_helper<__remove_cv_t<_Tp>>::type {};
template <> struct __is_integral_helper<bool> : public true_type {};
template <> struct __is_integral_helper<char> : public true_type {};
template <> struct __is_integral_helper<signed char> : public true_type {};
//...等等 文件靠枚举的方式为所有的整型特化了
using true_type = __bool_constant<true>;
//等价于integral_constant<bool, true>
template <bool __v>
using __bool_constant = integral_constant<bool, __v>;
template <typename _Tp, _Tp __v>
struct integral_constant {
static constexpr _Tp value = __v;
using value_type = _Tp;
using type = integral_constant<_Tp, __v>; //type还是它本身
constexpr operator value_type() const noexcept { return value; }
#ifdef __cpp_lib_integral_constant_callable // C++ >= 14
constexpr value_type operator()() const noexcept { return value; }
#endif
};
如果传入参数 _Tp
为整型, 比如说是 bool
,
那么 __is_integral_helper<_Tp>
等价于 true_type
,
它的成员 type
实际上也是 true_type
的一个别名
而 true_type
的 value
成员为静态 bool 常量表达式 true
enable_if
类型模板 enable_if
, 其类型模板原型如下
template< bool B, class T = void >
struct enable_if {};
template<class T = void >
struct enable_if<true, T>{
using type = T;
};
它是一个结构体, 并且对第一个参数进行特化
- 当
B
为true
时, 表示启用, 结构体中定义了类型别名using type = T
- 当
B
为false
时, 表示禁用T
, 结构体中没有类型别名type
enable_if_t
enable_if_t
是一个别名模板, 如下
template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;
- 当传入的
B
为true
时, 它返回T
(默认为void
) - 当传入的
B
为false
时, 它无法编译, 因为enable_if<false,T>::type
不存在.
例子
template <
typename T,
typename = std::enable_if_t<std::is_integral_v<T>
>
class X;
- 第二个类型参数是匿名的,
- 但它有默认实参
enable_if_t<std::is_integral_v<T>>
- 这个默认值 是用于检查类型
T
的, std::is_integral_v<T>
为false
时, 将无法编译
- 但它有默认实参
- 这约束了类型
T
必须是整型 - 因此实际上, 这个模板只有一个参数
T
由用户代码提供.
6.3 requires 子句和表达式
https://mariusbancila.ro/blog/2022/06/20/requires-expressions-and-requires-clauses-in-cpp20/
https://www.cnblogs.com/Cattle-Horse/p/16637811.html
requires 子句
https://en.cppreference.com/w/cpp/language/constraints#Requires_clauses
句法
requires 约束表达式
[[#约束表达式 constraint expression]] 的定义
可以使用 requires 子句直接约束模板类型
在对函数模板约束时, 可在两个位置上使用 requires 子句: 模板形参之后, 函数声明尾部.
template <typename T>
requires std::integral<T> || std::floating_point<T> //将类型T约束为整型或浮点类型
T func(T &t) {
return 1+t;
}
requires 表达式
它的结果是纯右值的 bool
类型 常量表达式,
可用于定义 concept
, 或者构成 requires 子句
句法
requires (形参列表 可缺省) {requirements序列}
- 如果不使用形参, 则形参列表可省缺
- 它不传入实参, 只是利用形参和形参的类型做检测. 它不是函数参数, 不能给默认值
- requirements 序列 是它检测的内容, 可以由多条语句构成, 将逐条检查是否通过编译.
- 要求序列可分为四种
- Simple requirements: 不以
requires
开头的任意表达式, 其断言表达式有效 - Type requirements: 以 typename 开头, 加类型名称. 将检查该类型是否存在 (不要求类型完整)
- [[#Compound requirements]]: 复合要求
- [[#Nested requirements]]: 嵌套要求
- Simple requirements: 不以
requires表达式不对序列内容求值, 但本身仍然求值, 并且是一个 bool 值.
例子
template <typename T>
concept C = requires (T a, T b) {
//简单requirement
a + b; //检查是否能相加
//类型requirement
typename T::inner; //检查是否存在类型别名
typename S<T>; //检查是否能实例化该模板
//复合requirement
{ a + b } noexcept->std::same_as<int>; //检查相加不抛出异常, 且返回类型为满足same_as<int>约束
//嵌套requirement
requires std::same_as<decltype((a+b)),int>; //嵌套一个requires子句
};
注意: requires 表达式不能单独在模板定义中使用
只有 requires 子句才能单独在模板定义中使用
要想使用 requires 表达式, 必须结合 requires 子句
例子
template <typename _Result>
requires requires { typename _Result::promise_type; }
struct __coroutine_traits_impl<_Result, void>
- 整体是一个
requires
子句, 后半部分是requires
表达式 - 当
_Result::promise_type
存在时, 该表达式返回true
Compound requirements
复合要求
句法
{ expression } noexcept(optional) return-type-requirement (optional) ;
其中 return-type-requirement
形如
-> type-constraint
- 模板实参将替换
expression
中的形参, 然后将检查expression
是否有效 - 其中
noexcept
是可选的, 将对expression
检查其不能抛出异常 return-type-requirement
用于约束和验证expression
的返回类型. 其返回类型应该使得type-constraint
返回true
- 返回类型约束
type-constraint
可以是 concept 或可用于定义 concept 的表达式 (常量bool
表达式).
例子
template<typename T>
concept myC = require (T x){
{x + 1} -> std::same_as<int>; //这是 复合需求Compound requirements
};
当 x+1
返回的是 int
类型时, 整个 requires
表达式值为 true
Nested requirements
嵌套要求, 通常使用 requires
子句,嵌套在主 requires
表达式的内部
范例: 对模板函数使用各种约束
约束可以在四个位置出现
- 用 concept 进行模板形参声明
- 形参声明后紧接的
requires
子句 - 受 concept 约束的占位符类型说明
- 函数声明尾部的
requires
子句
template <class C>
concept ConstType = std::is_const_v<C>;
template <class C>
concept IntegralType = std::is_integral_v<C>;
template <ConstType T> //concept约束
requires std::is_pointer_v<T> //requires子句
void foo(IntegralType auto) //concept约束
requires std::is_same_v<T, char * const> //声明尾后 requires子句
{
//函数定义
}
6.4 static_assert 声明
static_assert 用途
- 它不是宏/函数/运算符, 而是一种语言特性.
- 声明静态断言。如果断言失败,那么程序非良构,并且可能会生成诊断错误信息
- 这意味着, 只要断言失败, 则无法通过编译.
static_assert 可以在哪里使用
static_assert
可以在如下地方使用
- 全局作用域
- 命名空间的作用域
- 可以用于类的内部,通常用于确保类的某些属性在编译时满足
- 枚举类型
- 验证模板参数是否符合某些特定条件
- 函数体内
- 条件编译中
#if
static_assert 句法
static_assert( bool-constexpr , unevaluated-string )
static_assert( bool-constexpr )
static_assert( bool-constexpr , constant-expression )
1) 带有固定错误信息的静态断言。
2) 不带错误信息的静态断言。
3) 带有用户生成的错误信息的静态断言。
- 如果布尔常量表达式 良构并求值为
true
,或在模板定义的语境中求值而该模板未被实例化,那么该声明不做任何事情。 - 否则将发出编译时错误,并且诊断消息中会包含用户提供的错误信息(如果存在)。
- 包含
unevaluated-string
或者constant-expression
信息
- 包含
`static_assert` 只能对常量表达式检查, 例如下面的例子是错误的用法
例子
void f(int x){
static_assert(x>0,"haha"); //该声明不符合语法, x不是常量表达式
}
template <class _Ty>
constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept {
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"); //断言 _Arg不能是一个左值引用
return static_cast<_Ty&&>(_Arg);
}
和 requires 的区别
- 功能层面:
static_assert
是用于在编译时进行条件检查和验证的工具,主要用于简单的布尔表达式验证。requires
是更强大、灵活, 不仅用于表达式, 还可以检查类型, 语句合法等等.
- 使用范围:
static_assert
常用于各种编译时检查,不限于模板编程。- 而
requires
专门用于模板和概念的约束,尤其适合在泛型编程中使用。
和 noexcept 的区别
[[§4. 表达式概念和一些特殊表达式#noexcept说明符和运算符]]
不要和 noexcept 作用搞混淆
和 assert 的区别
- assert 定义在 cassert 中, 它是宏
- assert 是在程序运行时断言. 静态断言必须在编译期实现.
例子
#include <cassert>
int main(){
int x = 1;
int y = 2;
assert(x>y,"error");
}
无法运行, 断言失败
7 变量模板和 type traits
7.1 变量模板
变量模板简介
C++14 及更高版本中,引入了变量模板(variable templates)
该特性在 type_traits (类型特征)头文件中使用非常多.
该头文件用于检查模板类型实参是否符合要求
#include <iostream>
#include <type_traits>
// 定义一个模板变量,检查类型是否为整数类型
template<typename T>
constexpr bool is_integral_v = std::is_integral<T>::value;
int main() {
std::cout<< is_integral_v<float> << std::endl;
return 0;
}
is_integral<T>
是类型模板, value
是它的静态成员, 用于表示判断结果.
7.2 type traits 类型特征
type_traits 中的一些模板类型
![[Pasted image 20240716212642.png]]
type traits 原理
参考 [[#is_integral_v 和 is_integral]]
核心思想
- 类型萃取的实现主要依赖于模板特化和类型匹配规则,
- 通过为特定类型或类型组合提供特化的模板实现差异化操作.
- 在编译时根据 类型匹配规则 进行不同的操作, 从而萃取出类型的特征
- 它的编译时计算/决策, 利用了类型模板的编译计算/决策规则
type traits 用于 模板类型约束
可以利用 type_traits
中的类型模板 定义一个 concept
#include <iostream>
#include <concepts>
// 定义一个概念,要求类型必须是整数类型
template<typename T>
concept Integral = std::is_integral_v<T>;
// 使用概念约束模板参数
template<Integral T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << std::endl; // 正常工作
// std::cout << add(1.1, 2.2) << std::endl; // 编译错误,因为double不是整数类型
return 0;
}
8 元编程入门
c++20高级编程
什么是元编程?
在编译的时候就做一些事情, 比如计算和类型检查等
模板递归
vector<vector<int>>
就是一种模板递归. 然而这种写法不方便, 这里给出一种参考
template <typename T, size_t N>
class DNgrid {
std::vector<DNgrid<T,N-1>> data{};
public:
DNgrid(size_t k) : data(k, DNgrid<T,N-1>(k)){};
DNgrid() = default;
~DNgrid() = default;
const DNgrid<T, N - 1> &operator[](size_t i) const { return data[i]; }
DNgrid<T, N - 1> &operator[](size_t i) { return data[i]; }
void resize(size_t new_size) {
data.resize(new_size);
for (auto &e : data) {
e.resize(new_size);
}
}
};
template <typename T>
class DNgrid<T,1> { //特化版本, 作为递归基
std::vector<T> data{};
public:
DNgrid(size_t k) : data(k, T{}){};
DNgrid() = default;
~DNgrid() = default;
const T &operator[](size_t i) const { return data[i]; }
T &operator[](size_t i) { return data[i]; }
void resize(size_t new_size) { data.resize(new_size); }
};
template <typename T> class DNgrid<T,1>
是偏特化的版本, 作为递归基- 在构建一个 DNgrid 对象时, 会递归调用构造函数
- 使用 resize 时, 也会递归地修改每个维度的向量大小.
- 定义了两个
operator[]
, 一个是const
的版本, 用于对常量 NDgrid 对象的元素访问.
模板递归一般用偏特化技术定义递归基
例子: 编译时阶乘
#include <iostream>
template<int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
//特化递归基
template<>
struct Factorial<0> {
static const int value = 1;
};
int main() {
constexpr int result = Factorial<5>::value; // 计算 5! 的值
std::cout << "5! = " << result << std::endl;
return 0;
}
if constexpr 的使用
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << ’\n’;
if (sizeof...(Types)>0){
print(args...);
}
}
这个例子看上去很美好但是会发生错误
- 当打印到最后一个时, 参数包是空的. 此时程序看上去应该跳过
print(args...);
- 然而程序并不会跳过编译
print()
. 编译时永远会编译两个分支. - 而一个无参的
print
函数没有被定义, 产生了出错
只要使用 编译时的分支语句, 就能解决该问题
通过使用 if constexpr
, 编译时期可以跳过不成立的分支
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << ’\n’;
if constexpr(sizeof...(Types)>0){
print(args...);
}
}
9 type_traits 的conditional
_If<bool, >
//源码 clang
template <bool>
struct _IfImpl;
template <>
struct _IfImpl<true> {
template <class _IfRes, class _ElseRes>
using _Select = _IfRes;
}; //完全特化版本, 当条件为true时, _Select = _IfRes;
template <>
struct _IfImpl<false> {
template <class _IfRes, class _ElseRes>
using _Select = _ElseRes;
};
template <bool _Cond, class _IfRes, class _ElseRes>
using _If = typename _IfImpl<_Cond>::template _Select<_IfRes, _ElseRes>; //?
在最后一段, 第二个 template
是做什么的?
_Select
是类型模板 _IfImpl
内部的 别名模板, 编译器将其视为类型名 而非模板名, 因此要加一个 template
告诉编译器, 这是一个模板名字, 而非类型名字.
可以参考 <现代 c++核心特性解析>