待修改

1 定义模板

1.1 模板形参

模板参数

模板可以有两种参数, 一种是类型参数, 一种是非类型参数

这两种参数可以同时存在, 非类型参数 的类型 可以是 模板类型形参

template <
typename T, //1
T a //2
>
  • 第一个参数是类型参数 T
  • 第二个是非类型参数 a, 它的类型和形参 T 一致, 实际由传入实参决定

还有一种[[#1.5 模板的模板参数]]

模板的 类型参数
  • 像上面的T, 就是类型参数
  • 类型参数T, 可以作为 返回类型 或 参数类型 (的一部分)
  • 类型参数T, 也可用于在函数体内,变量声明, 类型转换

如何指定类型参数?

  • 使用 classtypename 来指定.
  • 可以指定多个类型参数, 用逗号分开
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 模板函数实例化 可以在编译器计算
  • todo

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类型进行特化

类模板的友元

类模板的友元
  • 友元和类模板是独立的
  • 友元可以是
    1. 函数, 类
    2. 函数模板, 类模板
    3. 模板函数/类 的 某个确定的 实例函数/类, 需要给定模板参数
    4. 模板函数/类 的某个 实例函数/类, 模板参数和类模板关联
    5. 注意: [[#类模板的部分特例化|部分特化]] 或它的别名 不能作为友元.

条款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
  1. 模板模板形参 T 是一个类型模板, 它有两个模板形参
  2. 模板形参 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_ptrshared_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);
}
  • 为何输出结果为 T, 这和书上解释的不一样? 为何实际的调用为 func<int>
利用 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), 从而引发无限递归和匹配失败
形参包语法

https://zh.cppreference.com/w/cpp/language/parameter_pack

模板形参包语法

  1. [类型] ... [包名(可选)]
template<int... args> // [类型] ... [包名(可选)]
void func() {
ifunc(args...);
}
  1. [typename|class] ... [包名(可选)]
template<class... Args> // [typename|class] ... [包名(可选)]
void func(Args... args) {
}
  1. [概念约束] ... [包名(可选)] (C++20 起)
template<std::integral... Ints> // 类型约束 ... 包名(可选)
void sum(Ints... values) {}
  1. template <形参列表> class ... [包名(可选)] (C++17 前)

这属于模板模板形参的写法

template<template<typename> class... Templates> // template < 形参列表 > class ... 包名(可选)
class Container {};
  1. 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);
  • todo 这里似乎写错了

    模式举例2
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 是左值, 因此模板推导 Tint &,
    • 从而 T&& 折叠为 int &,
    • static_cast<T&&> 等价于 static_cast<int &>, 将其转换为 int&
    • 然后调用左值引用版本的 show_type
  • perfect_forwarding(1)
    • 1 是右值, 因此模板推导 Tint,
    • 从而 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
}

该例中, 依然可以调用最匹配的版本.

大部分情况下, 还是不用担心 模板特化和重载的选择问题.

特例化时, 原模板声明必须可见
  • 当 特例化/偏特化 一个模板时, 它的原模板声明必须在作用域中
  • 这对于 类型模板/函数模板/别名模板 都一样
  • todo
特例化声明 的一些规则
  1. 特例化声明不可见, 而原模板可见时, 特例化可能失效

    • 编译器没有看见特例化定义, 将使用原模板 实例化
    • 从而绕过了用户在其他文件/作用域中声明的 特例化版本
  2. 特例化声明 必须 出现在 程序对模板实例化 之前
    • 如果程序在没有遇到特例化声明时, 就已经使用了模板, 那么编译器将生成一个 模板实例化的函数.
    • 这个实例化的函数 和 特例化的函数 如果参数都相同, 则发生冲突, 名字查找将发生歧义.
模板和特例化版本, 应放一个头文件中, 模板声明在前, 然后是特例化声明

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_typevalue 成员为静态 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;
};

它是一个结构体, 并且对第一个参数进行特化

  • Btrue 时, 表示启用, 结构体中定义了类型别名 using type = T
  • Bfalse 时, 表示禁用 T, 结构体中没有类型别名 type
enable_if_t

enable_if_t 是一个别名模板, 如下

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;
  • 当传入的 Btrue 时, 它返回 T (默认为 void)
  • 当传入的 Bfalse 时, 它无法编译, 因为 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 表达式

https://en.cppreference.com/w/cpp/language/requires

它的结果是纯右值的 bool 类型 常量表达式,

可用于定义 concept, 或者构成 requires 子句

句法

requires (形参列表 可缺省) {requirements序列}
  • 如果不使用形参, 则形参列表可省缺
  • 它不传入实参, 只是利用形参和形参的类型做检测. 它不是函数参数, 不能给默认值
  • requirements 序列 是它检测的内容, 可以由多条语句构成, 将逐条检查是否通过编译.
  • 要求序列可分为四种
    • Simple requirements: 不以 requires 开头的任意表达式, 其断言表达式有效
    • Type requirements: 以 typename 开头, 加类型名称. 将检查该类型是否存在 (不要求类型完整)
    • [[#Compound requirements]]: 复合要求
    • [[#Nested 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 声明

https://zh.cppreference.com/w/cpp/language/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 的使用

https://en.cppreference.com/w/cpp/language/if#Constexpr_if

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

  • todo

    定义了很多用于判断工具, 比如下面的 if
_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++核心特性解析>

16. C++快速入门--模板和Concept的更多相关文章

  1. webpack快速入门——CSS进阶:消除未使用的CSS

    使用PurifyCSS可以大大减少CSS冗余 1.安装 cnpm i purifycss-webpack purify-css --save-dev 2.引入glob,因为我们需要同步检查html模板 ...

  2. webpack快速入门——实战技巧:watch的正确使用方法,webpack自动打包

    随着项目大了,后端与前端联调,我们不需要每一次都去打包,这样特别麻烦,我们希望的场景是,每次按保存键,webpack自动为我们打包,这个工具就是watch! 因为watch是webpack自带的插件, ...

  3. webpack快速入门——CSS进阶:自动处理CSS3前缀

    为了浏览器的兼容性,有时候我们必须加入-webkit,-ms,-o,-moz这些前缀.目的就是让我们写的页面在每个浏览器中都可以顺利运行. 1.安装 cnpm i postcss-loader aut ...

  4. webpack快速入门——如何安装webpack及注意事项

    1.window+R键,输入cmd打开命令行工具,输入 mkdir XXXX(XX:文件夹名): 2.cd XXX 进入刚刚创建好的文件夹里,输入cnpm install -g webpack (安装 ...

  5. webpack快速入门——webpack3.X 快速上手一个Demo

    1.进入根目录,建两个文件夹,分别为src和dist 1).src文件夹:用来存放我们编写的javascript代码,可以简单的理解为用JavaScript编写的模块. 2).dist文件夹:用来存放 ...

  6. webpack快速入门——配置文件:入口和出口,多入口、多出口配置

    1.在根目录新建一个webpack.config.js文件,然后开始配置: const path = require('path'); module.exports={ //入口文件的配置项 entr ...

  7. webpack快速入门——配置文件:服务和热更新

    1.在终端安装 cnpm i webpack-dev-server --save-dev 2.配置好后执行 webpack-dev-server,这时候会报错 出现错误,只需要在pagejson里配置 ...

  8. webpack快速入门——CSS文件打包

    1.在src下新建css文件,在css文件下新建index.css文件,输入以下代码 body{ background:pink; color:yellowgreen; } 2.css建立好后,需要引 ...

  9. webpack快速入门——配置JS压缩,打包

    1 .首先在webpack.config.js中引入 const uglify = require('uglifyjs-webpack-plugin'); 2.然后在plugins配置里 plugin ...

  10. webpack快速入门——插件配置:HTML文件的发布

    1.把dist中的index.html复制到src目录中,并去掉我们引入的js 2.在webpack.config.js中引入 const htmlPlugin = require('html-web ...

随机推荐

  1. HDFS 重要机制之 checkpoint

    核心概念 hdfs checkpoint 机制对于 namenode 元数据的保护至关重要, 是否正常完成检查点是评估 hdfs 集群健康度和风险的重要指标 editslog : 对 hdfs 操作的 ...

  2. nginx关于正向代理与反向代理的概念区分

    正向代理:如果把局域网外的 Internet 想象成一个巨大的资源库,则局域网中的客户端要访问 Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理. 反向代理 反向代理中客户端对 ...

  3. JS 转盘抽奖效果

    阅读原文,微信扫描二维码, 手机关注公共号酒酒酒酒,搜索 JS 转盘抽奖效果 效果图 前置条件: 开发环境:windows 开发框架:js 编辑器:HbuilderX 正文开始 <!DOCTYP ...

  4. QT creator中cmake管理项目,如何引入外部库(引入Eigen库为例)

    在Eigen的官网下载压缩包[点我进入] 解压到当前项目的根目录(当然你也可以自己选择目录) 在当前项目的CMakeLists.txt任意位置加入这句话include_directories(${CM ...

  5. js实现浏览器后退页面刷新

    最近在开发中遇到一个问题: 在一个列表页面,点击进入详情,详情页面对其状态操作,其详情页面有做修改,然后点击浏览器后退,返回到列表页,在列表页面状态还是操作之前的,为解决状态统一需要手动刷新改列表页. ...

  6. Nginx支持https访问

    为了提高web应用的安全性,现在基本上都需要支持https访问.在此记录一下自己在nginx下的配置过程 安装Nginx这里就省略了 安装openssl模块 yum -y install openss ...

  7. CUDA编程学习 (4)——thread执行效率

    1. Warp 和 SIMD 硬件 1.1 作为调度单位的 Warp 每个 block 分为 32-thread warp 在 CUDA 编程模型中,虽然 warp 不是显式编程的一部分,但在硬件实现 ...

  8. python基础之__init__.py

    如何使用 在 Python 中,当一个目录被作为包来使用时,它会在包中寻找一个名为 __init__.py 的文件.如果该文件存在,Python 会将它加载到内存中,并在其中执行所有的代码. __in ...

  9. QT 6.8 安卓 Android 环境安装配置,你踩了几个坑,我教你跳出来,早看不入坑… …

    安装了QT6.8 最新版本,在线安装,用了数天后,想开始写一个Android程序,发现还在配置环境才可以继续,于是就开始配置: 菜单:编辑 -->preferences-->设备--> ...

  10. 2023NOIP A层联测25 T4 滈葕

    2023NOIP A层联测25 T4 滈葕 配血实验与2-SAT. 思路 \(z=1\) 表示配血实验发生凝集反应,设 \(a_i,b_i\) 分别表示第 \(i\) 个人有无凝集原 A,B.(无凝集 ...