首页 > 编程语言 >16. C++快速入门--模板和Concept

16. C++快速入门--模板和Concept

时间:2024-12-30 18:19:03浏览次数:1  
标签:std Concept 函数 16 -- int template 类型 模板

待修改

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 模板函数实例化 可以在编译器计算

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);
}
利用 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);
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
}

该例中, 依然可以调用最匹配的版本.
大部分情况下, 还是不用担心 模板特化和重载的选择问题.

特例化时, 原模板声明必须可见
  • 当 特例化/偏特化 一个模板时, 它的原模板声明必须在作用域中
  • 这对于 类型模板/函数模板/别名模板 都一样
特例化声明 的一些规则
  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

_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++核心特性解析>

标签:std,Concept,函数,16,--,int,template,类型,模板
From: https://www.cnblogs.com/easify/p/18642089

相关文章

  • java和php语言实现归并排序算法代码示例
    归并排序是一种经典的分治算法,它将数组分成两个子数组,分别进行排序,然后将它们合并成一个有序的数组。下面是用Java和PHP实现的归并排序算法:Java实现publicclassMergeSort{//主函数,用于调用归并排序publicstaticvoidmain(String[]args){int[]array......
  • MLEnd Deception Dataset
    TheMLEndDeceptionDataset ThisyearwearegoingtocreatetheMLEndDeceptionDataset,acollectionoftruthfulanddeceptivestoriesnarratedbyindividualsastheirownexperience,inEnglishandintheirnativelanguage.Wehopethatwhileworkin......
  • 如何利用无线路由器实现水泵房远程监测管理
    水泵站广泛部署应用在工农业用水、防洪、排涝和抗旱减灾等方面,如果水泵站发生异常,往往会对生产生活造成诸多损失,甚至引发安全事故。因此,建立一套高效、可靠的泵站远程监测管理系统至关重要。  方案背景 目前,我国大部分水泵房的数字化、信息化、智能化水平仍然较低,需要依托......
  • 【汇总】Android 版本号、版本名称、api版本、内核版本、发布日期
    一、说明网上有大佬,将相关内容整理了,但是每个版本都有一些没有信息,需要来回切换页面查看,所以将所有信息合并。方便查看。 二、表格 Android版本APILevelLinux内核版本代号首次发布日期后续Android版本支持截止日期Android1636 W   Android15356......
  • 3、RabbitMQ队列之工作队列【RabbitMQ官方教程】
    工作队列使用 php-amqplib 在第一个教程中,我们编写了从命名队列发送和接收消息的程序。在本例中,我们将创建一个工作队列,用于在多个工作人员之间分配耗时的任务。工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务,并必须等待其完成。相反,我们把任务安排在以后......
  • 中长期合约的曲线分解
     1.为什么必须进行电量的曲线分解电力市场化交易从现行交易过渡到现货交易,最大的区别就是电力现货交易的标的物,由传统的没有时空价值的指标性电量,转变为带时标的电力曲线。通俗的讲就是从之前的“量、价”交易到现在的“量、价、曲线”交易。中长期合约作为规避电力现货价格风......
  • WPF SoundPlayer
    //xaml<Windowx:Class="WpfApp121.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.mi......
  • Good Bye 2024 终究是败了
    写个题解。以后看一次后悔一次。TenderCarpenter不难发现,每个数单独一段一定是可行的。因为能够组成等边三角形。那么问题就变成了,能否分出一段长度不小于\(2\)的区间,使得其合法。显然的,\([l,r]\)的可行性不大于\([l+1,r]\)的可行性。那么枚举\(l=i,r=i+1\)判断是否合法......
  • Python+Django大学生入伍人员管理系统--(Pycharm Flask Django Vue mysql)
    收藏关注不迷路!!需要的小伙伴可以发链接或者截图给我项目介绍大学生入伍人员管理系统的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品,体验高科技时代带给人们的方便,同时也能让用户体会到与以往常规产品不同的体验风格。......
  • burp suite 6 (泷羽sec)
    声明学习视频来自B站UP主泷羽sec,如涉及侵泷羽sec权马上删除文章。笔记只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负这节课旨在扩大自己在网络安全方面的知识面,了解网络安全领域的见闻,了解学习哪些知识对于我们渗透......