C++20 Concepts简介
简介
在使用STL库的时候, 我们经常会遇到编译器的错误提示冗长且难以理解的情况. 这是因为编译器对模板的编译是分为两步, 第一步是模板的实例化,
使用用户提供的类型去替代模板参数, 第二步是对实例化后的代码进行编译. 编译器报告错误的时候往往是在第二步.
C++20引入了Concepts, 它是一种对模板进行约束的机制. Concept可以用在函数模板(Function Template), 类模板(Class Template), 通用函数成员(Generic Member Function)上. 能对模板参数, 函数参数进行约束. 约束作为接口的一部分, 允许编译器对其进行检查, 能让编译器更早发现错误, 提供更好的错误信息.
如何使用Concepts
有四种方式使用concepts
:
-
requires
语句 -
尾部的
requires
语句 -
受约束的模板参数
-
函数模板缩写
#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
requires integral<T> // Requires clause
auto max1(T a, T b) {
return a >= b ? a : b;
}
template <typename T>
auto max2(T a, T b)
requires integral<T> // Trailing requires clause
{
return a >= b ? a : b;
}
template <integral T> // Constrained template parameter
auto max3(T a, T b) {
return a >= b ? a : b;
}
auto max4(integral auto a, // Abbreviated function
integral auto b) // template
{
return a >= b ? a : b;
}
int main() {
cout << "max1(1,2) = " << max1(1, 2) << '\n';
cout << "max2(1,2) = " << max2(1, 2) << '\n';
cout << "max3(1,2) = " << max3(1, 2) << '\n';
cout << "max4(1,2) = " << max4(1, 2) << '\n';
}
代码输出:
max1(1,2) = 2
max2(1,2) = 2
max3(1,2) = 2
max4(1,2) = 2
Concepts使用场景
Concepts的使用场景有很多, 下面是一些常见的使用场景.
编译时谓词(Compile Time Predicates)
#include <iostream>
using namespace std;
template <typename T>
void checkIntegral(T value) {
if constexpr (integral<T>) { // compile-time predicate
cout << "The value is integral.\n";
} else {
cout << "The value is not integral.\n";
}
}
int main() {
checkIntegral(5); // The value is integral.
checkIntegral(5.5); // The value is not integral.
return 0;
}
类模板(Class Template)
#include <concepts>
template <std::regular T> // class template
class Container {
public:
void push_back(const T& item) {}
};
int main() {
Container<int> a; // OK
Container<int&> b; // ERROR: constraints not satisfied
}
成员函数模板
#include <concepts>
template <typename T>
struct Container {
void push_back(const T&)
requires std::copyable<T> // generic member function
{}
};
struct NotCopyable {
NotCopyable() = default;
NotCopyable(const NotCopyable&) = delete;
};
int main() {
Container<int> a;
a.push_back(2020);
Container<NotCopyable> b; // OK
b.push_back(NotCopyable()); // ERROR, requires copyable
}
可变参数模板
#include <concepts>
#include <iostream>
template <std::integral... Args>
bool all_positive(Args... args) {
return (... && (args > 0));
}
template <std::integral... Args>
bool any_positive(Args... args) {
return (... || (args > 0));
}
template <std::integral... Args>
bool none_positive(Args... args) {
return not(... || (args > 0));
}
int main() {
std::cout << std::boolalpha;
std::cout << " all positive: " << all_positive(-1, 0, 1) << '\n';
std::cout << " any positive: " << any_positive(-1, 0, 1) << '\n';
std::cout << "none positive: " << none_positive(-1, 0, 1) << '\n';
}
执行输出:
all positive: false
any positive: true
none positive: false
重载
#include <concepts>
#include <iostream>
using namespace std;
void fun(auto t) { cout << "fun(auto) : " << t << '\n'; }
void fun(long t) { cout << "fun(long) : " << t << '\n'; }
void fun(integral auto t) { cout << "fun(integral auto) : " << t << '\n'; }
int main() {
fun(2020.);
fun(2020L);
fun(2020);
}
执行输出:
fun(auto) : 2020
fun(long) : 2020
fun(integral auto) : 2020
模板特化(Template Specialization)
#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
struct Container {
Container() { cout << "Use Container<T>" << '\n'; }
};
template <regular T>
struct Container<T> {
Container() { cout << "Use Container<std::regular>" << '\n'; }
};
int main() {
Container<int> a;
Container<int&> b;
}
执行输出:
Use Container<std::regular>
Use Container<T>
使用多个Concepts
#include <concepts>
#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;
template <typename Iter, typename Val>
requires input_iterator<Iter> && // use multi concepts
same_as<typename iterator_traits<Iter>::value_type, Val>
bool contains(Iter b, Iter e, Val v) {
while (b != e && *b != v) ++b;
return b != e;
}
int main() {
vector vec{1, 2, 3, 4, 5};
cout << boolalpha;
cout << contains(vec.begin(), vec.end(), 5) << endl; // true
list list{1.1, 2.2, 3.3, 4.4, 5.5};
cout << contains(list.begin(), list.end(), 5.5) << endl; // true
return 0;
}
受限的和不受限的auto
C++14增加了泛型lambda(generic lambda), 但是不支持在函数模板中使用auto. 所谓泛型lambda是指使用auto
而不是具体的类型来定义lambda函数.
不受限的auto
单纯的auto被称为不受限的占位符,
下面的例子展示了不受限的占位符与相对应的模板函数.
#include <cassert>
template <typename L, typename R>
auto max_tmpl(L l, R r) {
return l >= r ? l : r;
}
auto max_auto(auto l, auto r) { return l >= r ? l : r; }
int main() {
assert(max_auto(1, 2) == max_tmpl(1, 2));
assert(max_auto(-1, 0) == max_tmpl(-1, 0));
}
受限的auto
受限的占位符是指使用 concepts
来约束auto
的类型.
下面的例子展示了受限的占位符与相对应的模板函数.
#include <cassert>
#include <concepts>
using namespace std;
template <integral L, integral R>
integral auto max_tmpl(L l, R r) {
return l >= r ? l : r;
}
integral auto max_auto(integral auto l, integral auto r) {
return l >= r ? l : r;
}
int main() {
assert(max_auto(1, 2) == max_tmpl(1, 2));
assert(max_auto(-1, 0) == max_tmpl(-1, 0));
}
Concepts库简介
一些常用的Concept定义在<concepts>
头文件中.
语言相关的 concepts
-
当且仅当
T
和U
是相同的类型时, 该concept才为真. -
当且仅当
T
是U
的派生类时, 该concept才为真. -
当且仅当
T
可以隐式转换为U
时, 该concept才为真. -
当且仅当
T
和U
有一个公共的引用类型时, 该concept才为真. -
当且仅当
T
和U
有一个公共的类型时, 该concept才为真. -
当且仅当
T
可以从U
赋值时, 该concept才为真. -
当且仅当
T
可以交换时, 该concept才为真.
std::same_as<T, U>
std::derived_from<T, U>
std::convertible_to<T, U>
std::common_reference_with<T, U>
std::common_with<T, U>
std::assignable_from<T, U>
std::swappable<T>
#include <concepts>
#include <iostream>
class Base {};
class Derived : public Base {};
template <typename L, typename R>
void same_as(L lhs, R rhs) {
if constexpr (std::same_as<L, R>) {
std::cout << "same type\n";
} else {
std::cout << "not same type\n";
}
}
template <typename D, typename B>
void derived_from(D d, B b) {
if constexpr (std::derived_from<D, B>) {
std::cout << "Derived from Base" << std::endl;
} else {
std::cout << "Not derived from Base" << std::endl;
}
}
template <typename L, typename R>
void convertible_to(L l, R r) {
if constexpr (std::convertible_to<L, R>) {
std::cout << "Convertible" << std::endl;
} else {
std::cout << "Not convertible" << std::endl;
}
}
template <typename L, typename R>
void common_reference_with(L& l, R& r) {
if constexpr (std::common_reference_with<L, R>) {
std::cout << "Common reference" << std::endl;
} else {
std::cout << "No common reference" << std::endl;
}
}
template <typename L, typename R>
void common_with(L& l, R& r) {
if constexpr (std::common_with<L, R>) {
std::cout << "Common" << std::endl;
} else {
std::cout << "Not common" << std::endl;
}
}
template <typename T, typename U>
void assignable_from(T& t, U& u) {
if constexpr (std::assignable_from<T&, U>) {
std::cout << "Assignable" << std::endl;
} else {
std::cout << "Not assignable" << std::endl;
}
}
template <typename T>
void swappable(T& t1, T& t2) {
if constexpr (std::swappable<T>) {
std::cout << "Swappable: " << t1 << " " << t2 << std::endl;
} else {
std::cout << "Not swappable" << std::endl;
}
}
int main() {
same_as(1, 3.14); // not same type
same_as(0, 1); // same type
Derived derived;
Base base;
derived_from(derived, base); // Derived from Base
derived_from(1, base); // Not derived from Base
convertible_to(1, 3.14); // Convertible
convertible_to(1, "1"); // Not convertible
int i = 1, j = 2;
double d = 3.14;
common_reference_with(i, d); // Common reference
common_reference_with(derived, base); // Common reference
common_reference_with(i, base); // No common reference
common_with(i, d); // Common
common_with(derived, base); // Common
common_with(i, base); // No common
assignable_from(i, j); // Assignable
assignable_from(derived, base); // Not assignable
assignable_from(base, derived); // Assignable
const int ci = 0;
assignable_from(ci, d); // Outputs: Not assignable
int l = 0, r = 1;
const int cl = 0, cr = 1;
swappable(l, r); // Swappable: 0, 1
swappable(cl, cr); // Not swappable
return 0;
}
数学 concepts
-
整数类型.
包含bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long, long long
,
以及对应的有符号和无符号类型,以及带const修饰符的类型. -
有符号整数类型, 满足
std::integral
且是带符号的 - 无符号整数类型
- 浮点数类型
std::integral
std::signed_integral
std::unsigned_integral
std::floating_point
#include <concepts>
#include <iostream>
using namespace std;
void print(std::signed_integral auto value) {
std::cout << "Signed Integral: " << value << '\n';
}
void print(std::unsigned_integral auto value) {
std::cout << "Unsigned Integral: " << value << '\n';
}
void print(std::floating_point auto value) {
std::cout << "Floating Point: " << value << '\n';
}
int main() {
print(10);
print(-5);
print(20u);
print(3.14);
}
运行输出:
Signed Integral: 10
Signed Integral: -5
Unsigned Integral: 20
Floating Point: 3.14
生命周期 concepts
- 默认初始化的
- 可拷贝构造的
- 可移动构造的
- 可构造的
- 可销毁的
std::default_initializable
std::copy_constructible
std::move_constructible
std::constructible_from
std::destructible
#include <concepts>
#include <iostream>
class Plain {};
class SingleInstance {
public:
SingleInstance() = default;
SingleInstance(const SingleInstance&) = delete;
SingleInstance(SingleInstance&&) = delete;
};
template <typename T>
class Container {
public:
void resize(size_t new_size)
requires std::default_initializable<T> && //
std::destructible<T> //
{}
void push_back(const T&)
requires std::copy_constructible<T> //
{}
void push_back(T&&)
requires std::move_constructible<T> //
{}
template <typename... Args>
void emplace_back(Args&&... args)
requires std::constructible_from<T, Args...> //
{}
};
int main() {
Container<Plain> a;
a.resize(10);
a.push_back(Plain());
a.emplace_back();
Container<SingleInstance> b;
b.resize(10); // OK
b.push_back(SingleInstance()); // ERROR, not copy constructible
b.emplace_back();
}
运行输出:
比较类的 concepts
-
相等性比较.是指结构体提供了等于
==
和!=
. 为了保证代码的逻辑正确,
通常只提供==
或者!=
. 另外一个完全可以由编译器推导出来. -
完全有序. 是指提供了比较运算符
<, <=, >, >=
.
同样,为了保证逻辑上的一致性,
四个比较操作符中只需要实现一个(通常是<
),
其他的关系完全可以推导出来.
std::equality_comparable
std::totally_ordered
只能用相等性而无法用有序性的场景,如:
C++中与nullptr
的比较,Python中与None
{.python}的比较等,数学上的与无穷大的比较等.
常见的关联容器可以分为两类: 基于比较的和基于哈希的.
基于比较的比如std::set, std::map, std::multiset, std::multimap
等.
通常其底层实现是红黑树(二分查找树的一种).
基于哈希的比如std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap
等.
通常其底层实现是哈希表. 哈希表的原理中需要计算键(Key)的哈希值,
所以键类型需要提供哈希函数. 在发生键冲突的时候, 为了保证键的唯一性,
还需要提供相等性比较函数来区别不同的键.
#include <concepts>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
using namespace std;
struct Equality {
string data;
bool operator==(const Equality& rhs) const { //
return data == rhs.data;
}
bool operator!=(const Equality& rhs) const { return !(*this == rhs); }
bool operator<(const Equality&) const = delete;
bool operator<=(const Equality&) const = delete;
bool operator>(const Equality&) const = delete;
bool operator>=(const Equality&) const = delete;
};
struct Ordered {
int a = 0;
bool operator<(const Ordered& rhs) const { //
return a < rhs.a;
}
bool operator>(const Ordered& rhs) const { return rhs < *this; }
bool operator==(const Ordered& rhs) const {
return !(*this < rhs) && !(rhs < *this);
}
bool operator<=(const Ordered& rhs) const { return !(rhs < *this); }
bool operator>=(const Ordered& rhs) const { return !(*this < rhs); }
};
int main() {
auto hash_fun = [](Equality const& key) {
return std::hash<std::string>()(key.data);
};
std::map<Ordered, int> map;
std::unordered_map<Equality, int, decltype(hash_fun)> hashmap;
std::set<Ordered> set;
std::unordered_set<Equality, decltype(hash_fun)> hashset;
map<Equality, int> em; // Error
unordered_map<Ordered, int> eum; // Error
}
对象相关的 concepts
-
正则的, 是指一个对象可以被复制(copyable),默认构造(default
constructible),以及比较相等性(equality comparable). - 半正则的, 比正则少了比较相等性的要求.
- 可移动的
- 可拷贝的
std::regular
std::semiregular
std::movable
std::copyable
#include <concepts>
#include <iostream>
// copyable, default constructible, and equality comparable.
struct Regular {
int a = 0;
bool operator==(const Regular& rhs) const { return a == rhs.a; };
};
// copyable, default constructible, but not equality comparable.
struct SemiRegular {
int a = 0;
bool operator==(const SemiRegular&) const = delete;
};
template <std::semiregular T>
void isSemiregular() {
std::cout << typeid(T).name() << " is semi-regular\n";
}
template <std::regular T>
void isRegular() {
std::cout << typeid(T).name() << " is regular\n";
}
int main() {
isSemiregular<SemiRegular>();
isRegular<Regular>();
isSemiregular<Regular>();
}
运行输出:
struct SemiRegular is semi-regular
struct Regular is regular
struct Regular is semi-regular
可调用的 concepts
- 可调用的
- 可调用且不改变入参, 另外一点就是对相同的输入参数会得到相同的输出.
- 谓词, 返回值为bool类型
std::invocable
std::regular_invocable
std::predicate
#include <concepts>
#include <iostream>
void print(int i) { std::cout << "Value: " << i << std::endl; }
bool is_even(int i) { return i % 2 == 0; }
template <std::invocable<int> Func>
void invocable_example(Func func) {
func(10);
}
template <std::regular_invocable<int> Func>
void regular_invocable_example(Func func) {
func(20);
}
template <std::predicate<int> Pred>
void predicate_example(Pred pred) {
if (pred(30)) {
std::cout << "Predicate returned true." << std::endl;
} else {
std::cout << "Predicate returned false." << std::endl;
}
}
int main() {
invocable_example(print);
regular_invocable_example(print);
predicate_example(is_even);
}
运行输出:
Value: 10
Value: 20
Predicate returned true.
工具类的库
- 输入迭代器
- 输出迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
- 连续迭代器
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
表达式的语法为:
requires (parameter-list(optional)) {requirement-seq}
其中parameter-list
是可选的, 用于指定requires
表达式的参数.
requirement-seq
是一个或多个requirement
的序列. 后文会详细介绍.
#include <string>
#include <vector>
template <typename T>
concept Arith = requires(T a, T b) { // simple requirement
a + b;
a - b;
a* b;
a / b;
};
int main() {
static_assert(Arith<int>); // OK
static_assert(Arith<double>); // OK
static_assert(Arith<std::string>); // Error
static_assert(Arith<std::vector<int>>); // Error
}
Concept Addable
要求类型T
支持+
操作符.
类型要求
在类型要求中, 必须使用关键字typename
和类型名.
#include <list>
#include <vector>
template <typename T>
struct Container {
using value_type = T;
};
template <typename T>
concept HasValueType = requires {
typename T::value_type; // type requirement
};
int main() {
static_assert(HasValueType<std::vector<int>>); // OK
static_assert(HasValueType<std::list<double>>); // OK
static_assert(HasValueType<Container<int>>); // OK
static_assert(HasValueType<int>); // Error
}
复合要求
复合要求的形式如下:
{expression} noexcept(optional) return-type-requirement(optional);
#include <concepts>
#include <iterator>
#include <list>
#include <vector>
template <typename T>
concept CompReq = requires(T t) { // compound requirement
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
int main() {
static_assert(CompReq<std::vector<int>>); // OK
static_assert(CompReq<std::list<double>>); // OK
static_assert(CompReq<int>); // Error
}
嵌套要求
#include <concepts>
#include <list>
#include <vector>
template <typename T>
concept Container = requires(T t) {
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template <typename T>
concept NestedReq = requires(T) { // nested requirement
Container<T>;
requires Arithmetic<typename T::value_type>;
};
int main() {
static_assert(NestedReq<std::vector<int>>); // OK
static_assert(NestedReq<std::list<double>>); // OK
static_assert(NestedReq<int>); // Error
}
总结
-
Concepts通过对类型参数施加约束来提高模板的可读性和可维护性.
-
Concepts可以应用于requires语句,尾部requires语句,受约束的模板参数和函数模板缩写.
-
Concepts是可以用作编译时谓词.可以基于Concepts进行重载,在Concepts上特化模板,在成员函数或可变参数模板上使用Concepts.
-
由于C++20和Concepts,使用未约束的占位符(auto)和约束的占位符(Concepts)是统一的.
-
由于新的缩写函数模板语法,定义函数模板变得非常简单.
-
不要重复发明轮子.在定义自己的Concepts之前,请研究C++20标准中丰富的预定义Concepts.
后记
笔者的原始文档为Latex格式, 转为makrdown
格式之后丢失了许多有用的样式, 如代码引用和代码高亮. 因此我上传了PDF格式的文档, 有兴趣的朋友可以下载查看.