C++模板
C++模板
模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。
所谓模板元编程,意思不过就是通过模板产生一堆函数罢了,是一种更高级别的抽象.计算机的任何事情果然都可以通过抽象和中间层化简和解决啊.
模板的一些规则
模板也可以重载.
各种类型推断不赘述,感兴趣可以看C++类型推断和类型推导规则
外部模板
传统 C++ 中,模板只要被实现后并且被使用就会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。
问题在于如果多个文件都使用了同个模板的同个实例,每个都会生成一遍,虽然链接的时候会剔除,但是生成时已经浪费了时间.
为此,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
以后就可以专门在一个地方实例化(注意不是定义)即可.
template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; // (1)
template 返回类型 名字 ( 形参列表 ) ; // (2)
extern template 返回类型 名字 < 实参列表 > ( 形参列表 ) ; // (3) (C++11 起)
extern template 返回类型 名字 ( 形参列表 ) ; // (4) (C++11 起)
- 显式实例化定义(显式指定所有无默认值模板形参时不会推导模板实参)
- 显式实例化定义,对所有形参进行模板实参推导
- 显式实例化声明(显式指定所有无默认值模板形参时不会推导模板实参)
- 显式实例化声明,对所有形参进行模板实参推导
template 类关键词 模板名 < 实参列表 > ; // (1)
extern template 类关键词 模板名 < 实参列表 > ;// (2)(C++11 起)
尖括号 ">"
在传统 C++ 的编译器中,>>
一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
std::vector<std::vector<int>> matrix;
这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:
template<bool T>
class MagicType {
bool magic = T;
};
// in main function:
std::vector<MagicType<(1>2)>> magic; // 合法, 但不建议写出这样的代码
函数模板
格式
template <typename AnyType>
void Swap(AnyType &a, AnyType &b){
AnyType temp;
temp = a;
a = b;
b = temp;
}
//template< 形参列表 > 函数声明
template
typename
都是必要的, 除非用class
替换typename
(两者等价),而且必须用尖括号.
变长参数模板
模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。 在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子, 接受一组固定数量的模板参数;而 C++11 加入了新的表示方法, 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
template<typename... Ts> class Magic;
模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;
既然是任意形式,所以个数为 0
的模板参数也是可以的:class Magic<> nothing;
。
如果不希望产生的模板参数个数为 0
,可以手动的定义至少一个模板参数:
template<typename Require, typename... Args> class Magic;
变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf
函数, 虽然也能达成不定个数的形参的调用,但其并非类别安全。 而 C++11 除了能定义类别安全的变长参数函数外, 还可以使类似 printf
的函数能自然地处理非自带类别的对象(下面就是一个实现定义)。 除了在模板参数中能使用 ...
表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数, 这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
template<typename... Args> void printf(const std::string &str, Args... args);//Args 为模板类型包,args为参数包,分别存储类型和值,名字可更换
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof...
来计算参数的个数,:
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以传递任意个参数给 magic
函数:
magic(); // 输出0
magic(1); // 输出1
magic(1, ""); // 输出2
其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
1. 递归模板函数
递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}//如果不写,那么下面的就会出现输出一个空参数的printf1,但是我们没有实现,故而会报错.
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
2. 变参模板展开
你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 printf
的编写:
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...); //注意这是编译期的事情,所以不能简单用if解决,故而17才成功了.
}
事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用
std::bind
及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。
3. 初始化列表展开
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
这里介绍一种使用初始化列表展开的黑魔法:
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
通过初始化列表,(lambda 表达式, value)...
将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。 为了避免编译器警告,我们可以将 std::initializer_list
显式的转为 void
。
[!NOTE]
需要注意,这里之所以能够实现是因为
…
是作用与一个表达式的,或者说就是代表要展开参数.而lambda作为一个表达式就自然反复被调用.再通过initializer_list变成列表强制在编译期展开.
折叠表达式
C++ 17 中将变长参数这种特性进一步带给了表达式,考虑下面这个例子:
#include <iostream>
template<typename ... T>
auto sum(T ... t) {
return (t + ...);//此处则是...的另一个用法,不过核心还是展开参数
}
int main() {
std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
}
( 形参包 运算符 ... ) (1)
( ... 运算符 形参包 ) (2)
( 形参包 运算符 ... 运算符 初值 ) (3)
( 初值 运算符 ... 运算符 形参包 ) (4)
- 一元右折叠
- 一元左折叠
- 二元右折叠
- 二元左折叠
分别的例子
#include <iostream>
// 一元右折叠:(a + ...) 相当于 a1 + (a2 + (a3 + ...))
template<typename... Args>
auto sum_right(Args... args) {
return (args + ...);
}
int main() {
std::cout << sum_right(1, 2, 3, 4) << std::endl; // 1+(2+(3+4))=10
return 0;
}
// 一元左折叠:(... * a) 相当于 (((a1 * a2) * a3) * ...)
template<typename... Args>
auto product_left(Args... args) {
return (... * args);
}
int main() {
std::cout << product_left(1, 2, 3, 4) << std::endl; // ((1*2)*3)*4=24
return 0;
}
// 二元右折叠:(a + ... + 10) 相当于 a1 + (a2 + (a3 + 10))
template<typename... Args>
auto sum_right_with_init(Args... args) {
return (args + ... + 10);
}
int main() {
std::cout << sum_right_with_init(1, 2, 3) << std::endl; // 1+(2+(3+10))=16
return 0;
}
// 二元左折叠:(100 - ... - a) 相当于 (((100 - a1) - a2) - a3)
template<typename... Args>
auto subtract_left_with_init(Args... args) {
return (100 - ... - args);
}
int main() {
std::cout << subtract_left_with_init(10, 20, 30) << std::endl; // ((100-10)-20)-30=40
return 0;
}
有默认实参的模板类型形参
格式:
template<typename T = int>
void f();
f(); // 默认为 f<int>
f<double>(); // 显式指明为 f<double>
有些好玩的:
using namespace std::string_literals;
template<typename T1,typename T2,typename RT =
decltype(true ? T1{} : T2{}) >
RT max(const T1& a, const T2& b) { // RT 是 std::string
return a > b ? a : b;
}
可以这样写(是一种落伍的写法,只是了解一二)
这里decltype(true ? T1{}: T2{})
什么意思呢?,就是三目表达式要求第二项和第三项之间能够隐式转换,然后整个表达式的类型会是 “公共”类型。也就是最能概括两者的类型.
至于T{}
带着花括号是用来构造临时对象,通过这个形式可以获得一个类型.(C++规则真离谱)
非类型模板参数推导
前面我们主要提及的是模板参数的一种形式:类型模板参数。
template <typename T, typename U>
auto add(T t, U u) {
return t+u;
}
其中模板的参数 T
和 U
为具体的类型。 但还有一种常见模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:
template <typename T, int BufSize>
class buffer_t {
public:
T& alloc();
void free(T& item);
private:
T data[BufSize];
}
buffer_t<int, 100> buf; // 100 作为模板参数
在这种模板参数形式下,我们可以将 100
作为模板的参数进行传递。 在 C++11 引入了类型推导这一特性后,我们会很自然的问,既然此处的模板参数 以具体的字面量进行传递,能否让编译器辅助我们进行类型推导, 通过使用占位符 auto
从而不再需要明确指明类型? 幸运的是,C++17 引入了这一特性,我们的确可以 auto
关键字,让编译器辅助完成具体类型的推导, 例如:
template <auto value> void foo() {
std::cout << value << std::endl;
return;
}
int main() {
foo<10>(); // value 被推导为 int 类型
}
类模板
类模板不是类,只有实例化类模板,编译器才能生成实际的类。
语法和函数模板差不多
模板成员函数
普通类和模板类都可以有模板成员函数.
用户定义的推导指引
举个例子,我要让一个类模板,如果推导为 int,就让它实际成为 size_t:
template<typename T>
struct Test{
Test(T v) :t{ v } {}
private:
T t;
};
Test(int) -> Test<std::size_t>;
Test t(1); // t 是 Test<size_t>
如果要类模板 Test
推导为指针类型,就变成数组呢?
template<typename T>
Test(T*) -> Test<T[]>;
char* p = nullptr;
Test t(p); // t 是 Test<char[]>
推导指引的语法还是简单的,如果只是涉及具体类型,那么只需要:
模板名称(类型a)->模板名称<想要让类型a被推导为的类型>
如果涉及的是一类类型,那么就需要加上 template
,然后使用它的模板形参。
变量模板
template<typename T>
T v; //可以有各种修饰
模板全特化
给出这样一个函数模板 f
,你可以看到,它的逻辑是返回两个对象相加的结果,那么如果我有一个需求:“如果我传的是一个 double 一个 int 类型,那么就让它们返回相减的结果”。
template<typename T,typename T2>
auto f(const T& a, const T2& b) {
return a + b;
}
C++14 允许函数返回声明的 auto 占位符自行推导类型。
这种定制的需求很常见,此时我们就需要使用到模板全特化:
template<>
auto f(const double& a, const int& b){
return a - b;
}
当特化函数模板时,如果模板实参推导能通过函数实参提供,那么就可以忽略它的模板实参。
语法很简单,只需要先写一个 template<>
后面再实现这个函数即可。
不过我们其实有两种写法的,比如上面那个示例,我们还可以写明模板实参。
template<>
auto f<double, int>(const double& a, const int& b) {
return a - b;
}
个人建议写明更加明确,因为很多时候模板实参只是函数形参类型的一部分而已,比如上面的 const double&
、const int&
只有 double
、int
是模板实参。
模板偏特化
模板偏特化这个语法让模板实参具有一些相同特征可以自定义,而不是像全特化那样,必须是具体的什么类型,什么值。
比如:指针类型,这是一类类型,有 int*
、double*
、char*
,以及自定义类型的指针等等,它们都属于指针这一类类型;可以使用偏特化对指针这一类类型进行定制。
- 模板偏特化使我们可以对具有相同的一类特征的类模板、变量模板进行定制行为。
举例(变量模板):
template<typename T>
const char* s = "?"; // 主模板
template<typename T>
const char* s<T*> = "pointer"; // 偏特化,对指针这一类类型
template<typename T>
const char* s<T[]> = "array"; // 偏特化,但是只是对 T[] 这一类类型,而不是数组类型,因为 int[] 和 int[N] 不是一个类型
std::cout << s<int> << '\n'; // ?
std::cout << s<int*> << '\n'; // pointer
std::cout << s<std::string*> << '\n'; // pointer
std::cout << s<int[]> << '\n'; // array
std::cout << s<double[]> << '\n'; // array
std::cout << s<int[1]> << '\n'; // ?
待决名
简单说,待决名就是模板里 “意义暂时定不下来的名字”。因为模板要等实例化时才知道具体类型,有些名字(比如T::type
、S<T>::foo
)在写模板时没法确定它到底是类型、变量还是函数模板,这种 “悬而未决” 的名字就叫待决名。
为了让编译器正确理解待决名,C++ 引入了两个关键字:
typename
:告诉编译器 “这是个类型”template
:告诉编译器 “这是个模板”
typename
:解决 “是不是类型” 的歧义
当你在模板里用X::Y
这种形式,且X
依赖模板参数时,编译器默认不认为Y
是类型,必须用typename
声明。下例:
template<typename T>
void func() {
// T::type 是待决名,必须用typename声明它是类型
typename T::type value; // 正确:告诉编译器T::type是类型
// T::type value; // 错误:编译器会以为type是变量
}
// 测试用的类型
struct MyType {
using type = int; // 这里type是类型别名
};
int main() {
func<MyType>(); // 实例化后,T::type就是int
return 0;
}
#include <vector>
template<typename T>
void print(const std::vector<T>& v) {
// std::vector<T>::iterator是待决名,必须加typename
typename std::vector<T>::iterator it = v.begin(); // 正确
// std::vector<T>::iterator it; // 错误:编译器不认它是类型
}
template
:解决 “是不是模板” 的歧义
当你在模板里用X::Y<...>
这种形式,且X
依赖模板参数时,编译器默认不认为Y
是模板,必须用template
声明。下例:
template<typename T>
struct MyStruct {
template<typename U>
void foo(U x) { // 成员函数模板
cout << x << endl;
}
};
template<typename T>
void call_foo() {
MyStruct<T> s;
// s.foo<int>(5); // 错误:编译器会把<当成小于号
s.template foo<int>(5); // 正确:用template声明foo是模板
}
int main() {
call_foo<int>(); // 输出5
return 0;
}
template<typename T>
struct Container {
template<typename U>
using SubType = pair<T, U>; // 嵌套的模板类型
};
template<typename T>
void test() {
// 访问Container<T>的嵌套模板SubType
typename Container<T>::template SubType<double> data; // 正确
// 解释:
// 1. typename:声明Container<T>::SubType是类型
// 2. template:声明SubType是模板(需要传入double)
}
待决名的查找规则
- 非待决名:在模板定义时就确定含义,后面加新定义也不影响。
- 待决名:在模板实例化时才确定含义,会找实例化时可见的定义。
#include <iostream>
// 全局函数
void print(int x) { cout << "全局print: " << x << endl; }
template<typename T>
struct Base {
void print(int x) { cout << "Base::print: " << x << endl; }
};
template<typename T>
struct Derived : Base<T> {
void call_print() {
print(1); // 非待决名:定义时绑定到全局print
this->print(2); // 待决名:实例化时找Base<T>的print
}
};
int main() {
Derived<int> d;
d.call_print();
// 输出:
// 全局print: 1
// Base::print: 2
return 0;
}
解释:
print(1)
:非待决名,在Derived
定义时就找到了全局的print
,绑死了。this->print(2)
:this
依赖模板参数T
,所以是待决名,实例化时才去Base<T>
里找print
。
总结
- 待决名是模板中 “含义暂时不确定” 的名字,依赖模板参数。
- 用
typename
声明待决名是类型(如typename T::type
)。 - 用
template
声明待决名是模板(如s.template foo<int>()
)。 - 非待决名在定义时绑定,待决名在实例化时才查找。
记住:看到X::Y
且X
是模板参数,先想是不是需要typename
;看到X::Y<...>
,再想是不是需要template
。
SFINAE
“代换失败不是错误” (Substitution Failure Is Not An Error)
在函数模板的重载决议中会应用此规则:当编译器尝试用具体类型替换模板参数时,如果替换失败了,不会直接报错,而是会忽略这个不匹配的模板,继续找其他可能的重载版本。
此特性被用于模板元编程。
核心概念:代换失败 vs 硬错误
- 代换失败:替换模板参数时,在 "立即语境"(比如函数参数类型、返回类型)中发现不合法(比如
T::type
不存在),这是 SFINAE 错误,只会排除当前模板。 - 硬错误:替换后触发了副作用(比如实例化了另一个模板导致错误),这是真正的错误,会导致编译失败。
下例:
#include <iostream>
// 模板1:要求T必须有type成员
template<typename T>
void func(typename T::type) {
std::cout << "T有type成员" << std::endl;
}
// 模板2:通用版本
template<typename T>
void func(T) {
std::cout << "通用版本" << std::endl;
}
struct HasType { using type = int; };
struct NoType {};
int main() {
func<HasType>(1); // 调用模板1:HasType有type
func<NoType>(2); // 调用模板2:NoType无type,模板1代换失败但不报错
return 0;
}
//输出:
//T有type成员
//通用版本
标准库工具
C++ 标准库提供了几个工具简化 SFINAE 写法:
std::enable_if
:条件满足才启用模板
#include <type_traits>
// 只有T是int时才启用这个函数
template<typename T>
std::enable_if_t<std::is_same_v<T, int>, void>
print(T x) {
std::cout << "int: " << x << std::endl;
}
// 只有T是double时才启用这个函数
template<typename T>
std::enable_if_t<std::is_same_v<T, double>, void>
print(T x) {
std::cout << "double: " << x << std::endl;
}
int main() {
print(10); // 匹配int版本
print(3.14); // 匹配double版本
// print("hi"); // 无匹配版本,编译错误
return 0;
}
原理:std::enable_if<条件, 类型>
只有条件为true
时才有type
成员,否则代换失败。
std::void_t
:检查类型是否有特定成员
#include <type_traits>
// 检查T是否有size()成员函数
template<typename T, typename = void>
struct HasSize : std::false_type {};
template<typename T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
// 测试
#include <vector>
#include <iostream>
int main() {
std::cout << HasSize<std::vector<int>>::value << std::endl; // 1(有size())
std::cout << HasSize<int>::value << std::endl; // 0(无size())
return 0;
}
原理:std::void_t<...>
接受任意类型,只要里面的表达式合法就不会失败,否则代换失败。
std::declval
:无需构造对象即可调用成员
#include <type_traits>
// 检查T是否有operator+,即使T没有默认构造函数
template<typename T>
struct HasPlus {
template<typename U>
static auto check(U u) -> decltype(u + u, std::true_type{});
static std::false_type check(...);
static constexpr bool value = decltype(check(std::declval<T>()))::value;
};
struct A { int operator+(A) { return 0; } };
struct B { B(int) {} int operator+(B) { return 0; } }; // 无默认构造
int main() {
std::cout << HasPlus<A>::value << std::endl; // 1(A有+且可默认构造)
std::cout << HasPlus<B>::value << std::endl; // 1(B有+,用declval避开构造)
return 0;
}
原理:std::declval<T>()
能在不构造T
对象的情况下生成一个T
类型的引用,方便在decltype
中调用成员。
为什么需要 SFINAE?
- 精确控制重载:让编译器只选择符合条件的模板版本。
- 友好的错误提示:替换失败时编译器会说 "找不到匹配的函数",而不是报一堆模板实例化错误。
- 编译期类型检查:在编译时就能确认类型是否满足要求(比如是否有某个成员)。
约束和概念
概念
C++20的引入的.
有了它,我们的模板可以有更多的静态检查,语法更加美观,写法更加容易,而不再需要利用古老的 SFINAE。
请务必学习完了上一章节内容;本节会一边为你教学约束与概念的语法,一边用 SFINAE 对比,让你意识到:这是多么先进、简单的核心语言特性。
template<typename T>
concept Add = requires(T a) {
a + a; // "需要表达式 a+a 是可以通过编译的有效表达式"
};
template<Add T>
auto add(const T& t1, const T& t2){
std::puts("concept +");
return t1 + t2;
}
这里的Add就是一个概念,只要概念合法,就可行.
每个概念都是一个谓词,它在编译时求值,并在将之用作约束时成为模板接口的一部分。
也就是可以下面如此:
std::cout << std::boolalpha << Add<int> << '\n'; // true
std::cout << std::boolalpha << Add<char[10]> << '\n'; // false
constexpr bool r = Add<int>; // true
进一步发展:
decltype(auto) max(const auto& a, const auto& b) { // const T&
return a > b ? a : b;
}
我想要约束:传入的对象 a b 必须都是整数类型,应该怎么写?。
#include <concepts> // C++20 概念库标头
decltype(auto) max(const std::integral auto& a, const std::integral auto& b) {
return a > b ? a : b;
}
max(1, 2); // OK
max('1', '2'); // OK
max(1u, 2u); // OK
max(1l, 2l); // OK
max(1.0, 2); // Error! 未满足关联约束
如你所见,我们没有自己定义 概念(concept),而是使用了标准库的 std::integral
,它的实现非常简单:
template< class T >
concept integral = std::is_integral_v<T>;
这也告诉各位我们一件事情:定义概念(concept) 时声明的约束表达式,只需要是编译期可得到 bool
类型的表达式即可。
总结一下,无论模板还是auto,都可以用概念约束.只要编译器可以得到bool类型就可以作为概念
requires
子句
关键词 requires
用来引入 requires
子句,它指定对各模板实参,或对函数声明的约束。
也就是说我们多了一种使用概念(concept)或者说约束的写法。
template<typename T>
concept add = requires(T t) {
t + t;
};
template<typename T>
requires std::is_same_v<T, int>
void f(T){}
template<typename T> requires add<T>
void f2(T) {}
template<typename T>
void f3(T)requires requires(T t) { t + t; }
{}
requires
子句期待一个能够编译期产生bool
值的表达式。
以上示例展示了 requires
子句的用法,我们一个个解释
f
的requires
子句写在template
之后,并空四格,这是我个人推荐的写法;它的约束是:std::is_same_v<T, int>
,意思很明确,约束T
必须是 int 类型,就这么简单。f2
的requires
子句写法和f
其实是一样的,只是没换行和空格;它使用了我们自定义的概念(concept)add
,约束T
必须满足add
。f3
的requires
子句在函数声明符的末尾元素出现;这里我们连用了两个requires
,为什么?其实很简单,我们要区分,第一个requires
是requires
子句,第二个requires
是约束表达式,它会产生一个编译期的bool
值,没有问题。(如果T
类型带入约束表达式是良构,那么就返回true
、否则就返回false
)。
类模板、变量模板等也都同理
requires 子句中,关键词 requires 必须后随某个常量表达式。
template<typename T>
requires true
void f(T){}
完全可行,各位其实可以直接带入,说白了 requires
子句引入的约束表,必须是可以编译期返回 bool
类型的值的表达式,我们前面的三个例子:std::is_same_v
、add
、requires 表达式
都如此。
约束
合取
两个约束的合取是通过在约束表达式中使用 && 运算符来构成的:
template<class T>
concept Integral = std::is_integral_v<T>;
template<class T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
template<class T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
很合理,约束表达式可以使用 &&
运算符连接两个约束,只有在两个约束都被满足时才会得到满足
我们先定义了一个 概念(concept)Integral,此概念要求整形;又定义了概念(concept)SignedIntegral,它的约束表达式用到了先前定义的概念(concept)Integral,然后又加上了一个 &&
还需要满足 std::is_signed_v。
概念(concept)SignedIntegral
是要求有符号整数类型,它的约束表达式是:Integral<T> && std::is_signed_v<T>
,就是这个表达式要返回 true
才成立,就这么简单。
析取
两个约束的析取,是通过在约束表达式中使用 || 运算符来构成的:
template<typename T>
concept number = std::integral<T> || std::floating_point<T>;
和 ||
运算符本来的意思一样, std::integral<T>
、std::floating_point
满足任意一个,那么整个约束表达式就都得到满足。
void f(const number auto&){}
f(1); // OK
f(1u); // OK
f(1.2); // OK
f(1.2f); // OK
f("1"); // 未找到匹配的重载函数
requires
表达式
没气力了,以后用到再说