C++20 Concepts简介
简介
在使用 STL 库的时候, 我们经常会遇到编译器的错误提示冗长且难以理解的情况. 这是因为编译器对模板的编译是分为两步, 第一步是模板的实例化, 使用用户提供的类型去替代模板参数, 第二步是对实例化后的代码进行编译. 编译器报告错误的时候往往是在第二步.
C++20 引入了 Concepts, 它是一种对模板进行约束的机制. Concept 可以用在函数模板(Function Template), 类模板(Class Template), 通用函数成员(Generic Member Function)上. 能对模板参数, 函数参数进行约束. 约束作为接口的一部分, 允许编译器对其进行检查, 能让编译器更早发现错误, 提供更好的错误信息.
如何使用 Concepts
有四种方式使用concepts
:
requires
语句- 尾部的
requires
语句 - 受约束的模板参数
- 函数模板缩写
代码输出:
Concepts 使用场景
Concepts 的使用场景有很多, 下面是一些常见的使用场景.
编译时谓词(Compile Time Predicates)
类模板(Class Template)
成员函数模板
可变参数模板
执行输出:
重载
执行输出:
模板特化(Template Specialization)
执行输出:
使用多个 Concepts
受限的和不受限的auto
C++14 增加了泛型 lambda(generic lambda), 但是不支持在函数模板中使用 auto.
所谓泛型 lambda 是指使用auto
而不是具体的类型来定义 lambda 函数.
不受限的 auto
单纯的 auto 被称为不受限的占位符, 下面的例子展示了不受限的占位符与相对应的模板函数.
受限的 auto
受限的占位符是指使用 concepts
来约束auto
的类型.
下面的例子展示了受限的占位符与相对应的模板函数.
Concepts 库简介
一些常用的 Concept 定义在<concepts>
头文件中.
语言相关的 concepts
std::same_as<T, U>
: 当且仅当T
和U
是相同的类型时, 该 concept 才为真.std::derived_from<T, U>
: 当且仅当T
是U
的派生类时, 该 concept 才为真.std::convertible_to<T, U>
: 当且仅当T
可以隐式转换为U
时, 该 concept 才为真.std::common_reference_with<T, U>
: 当且仅当T
和U
有一个公共的引用类型时, 该 concept 才为真.std::common_with<T, U>
: 当且仅当T
和U
有一个公共的类型时, 该 concept 才为真.std::assignable_from<T, U>
: 当且仅当T
可以从U
赋值时, 该 concept 才为真.std::swappable<T>
: 当且仅当T
可以交换时, 该 concept 才为真.
数学 concepts
std::integral
: 整数类型. 包含bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long, long long
, 以及对应的有符号和无符号类型,以及带 const 修饰符的类型.std::signed_integral
: 有符号整数类型, 满足std::integral
且是带符号的std::unsigned_integral
: 无符号整数类型std::floating_point
: 浮点数类型
运行输出:
生命周期 concepts
std::default_initializable
: 默认初始化的std::copy_constructible
: 可拷贝构造的std::move_constructible
: 可移动构造的std::constructible_from
: 可构造的std::destructible
: 可销毁的
运行输出:
比较类的 concepts
std::equality_comparable
: 相等性比较.是指结构体提供了等于==
和!=
. 为了保证代码的逻辑正确, 通常只提供==
或者!=
. 另外一个完全可以由编译器推导出来.std::totally_ordered
: 完全有序. 是指提供了比较运算符<, <=, >, >=
. 同样,为了保证逻辑上的一致性, 四个比较操作符中只需要实现一个(通常是<
), 其他的关系完全可以推导出来.
只能用相等性而无法用有序性的场景, 如: C++中与nullptr
的比较, 数学上的与无穷大的比较等.
常见的关联容器可以分为两类: 基于比较的和基于哈希的.
- 基于比较的比如
std::set, std::map, std::multiset, std::multimap
等. 通常其底层实现是红黑树(二分查找树的一种). - 基于哈希的比如
std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap
等. 通常其底层实现是哈希表. 哈希表的原理中需要计算键(Key)的哈希值, 所以键类型需要提供哈希函数. 在发生键冲突的时候, 为了保证键的唯一性, 还需要提供相等性比较函数来区别不同的键.
对象相关的 concepts
std::regular
: 正则的, 是指一个对象可以被复制(copyable),默认构造(default constructible), 以及比较相等性(equality comparable).std::semiregular
: 半正则的, 比正则少了比较相等性的要求.std::movable
: 可移动的std::copyable
: 可拷贝的
运行输出:
可调用的 concepts
std::invocable
: 可调用的std::regular_invocable
: 可调用且不改变入参, 另外一点就是对相同的输入参数会得到相同的输出.std::predicate
: 谓词, 返回值为 bool 类型
运行输出:
工具类的库
std::input_iterator
: 输入迭代器std::output_iterator
: 输出迭代器std::forward_iterator
: 前向迭代器std::bidirectional_iterator
: 双向迭代器std::random_access_iterator
: 随机访问迭代器std::contiguous_iterator
: 连续迭代器
算法相关的 concepts
std::permutable
: 可在原地排序, 不需要额外的内存.std::mergeable
: 可以合并两个有序序列到输出序列.std::sortable
: 可形成有序序列.
自定义requires
表达式
C++20 支持自定义 Concepts, 目前有四种方法.
简单要求
requires
表达式的语法为:
其中parameter-list
是可选的, 用于指定requires
表达式的参数. requirement-seq
是一个或多个requirement
的序列. 后文会详细介绍.
Concept Addable
要求类型T
支持+
操作符.
类型要求
在类型要求中, 必须使用关键字typename
和类型名.
复合要求
复合要求的形式如下:
嵌套要求
总结
- Concepts 通过对类型参数施加约束来提高模板的可读性和可维护性.
- Concepts 可以应用于 requires 语句,尾部 requires 语句,受约束的模板参数和函数模板缩写.
- Concepts 是可以用作编译时谓词.可以基于 Concepts 进行重载,在 Concepts 上特化模板,在成员函数或可变参数模板上使用 Concepts.
- 由于 C++20 和 Concepts,使用未约束的占位符(auto)和约束的占位符(Concepts)是统一的.
- 由于新的缩写函数模板语法,定义函数模板变得非常简单.
- 不要重复发明轮子. 在定义自己的 Concepts 之前,请研究 C++20 标准中丰富的预定义 Concepts.