C++20, Concept And Range
Table of Contents
1. Concept
1.1. 动机
当我们使用模板进行通用编程时,常常需要定义能够被各种类型使用的函数和类。但是,在实例化模板时经常会出现用错类型的问题,这往往导致编译器返回大量难懂的报错信息。C++20 中引入了 Concept,这个问题可以较好地解决。 Concept 让你能为模板编写要求,而编译器则可以检查这个要求。
1.2. 使用 Concept
在介绍如何自定义 Concept 前,我们先介绍一下如何使用标准库中自带的 Concept,如 std::integral。
形式 1:在模板中把关键字 typename 换为对应的 Concept 名字。如:
#include <iostream>
#include <concepts> // 需要编译器支持C++20,如GCC 10.0
template <std::integral t1, std::integral t2> // 类型t1 和 t2 必须满足 Concept std::integral
auto add(t1 const v1, t2 const v2)
{
return v1 + v2;
}
int main()
{
auto x = add(1, 2);
std::cout << x << std::endl;
// auto y = add(1.1, 2.2); // 报错。add 参数不满足 Concept std::integral
// std::cout << y << std::endl;
}
形式 2:省略 template,直接把 Concept 写到函数参数中。如:
auto add(std::integral auto const v1, std::integral auto const v2)
{
return v1 + v2;
}
形式 3:使用 requires 指定 Concept。如:
template <typename t1, typename t2>
requires std::integral<t1> && std::integral<t2>
auto add(t1 const v1, t2 const v2)
{
return v1 + v2;
}
requires 也可以写在函数参数列表的后面,如:
template <typename t1, typename t2>
auto add(t1 const v1, t2 const v2) requires std::integral<t1> && std::integral<t2>
{
return v1 + v2;
}
如果要对一个类型同时指定多个 Concept,则只能使用形式 3。比如,指定类型 t1,t2 满足 std::integral 或者 std::floating_point 中的一个即可:
#include <iostream>
#include <concepts> // 需要编译器支持C++20,如GCC 10.0
template <typename t1, typename t2>
requires (std::integral<t1> || std::floating_point<t1> ) && (std::integral<t2> || std::floating_point<t2>)
auto add(t1 const v1, t2 const v2)
{
return v1 + v2;
}
int main()
{
auto x = add(1, 2);
std::cout << x << std::endl;
auto y = add(1.1, 2.2); // 这里不再会报错。
std::cout << y << std::endl;
}
1.3. 定义 Concept
Concept 由下面语法来定义:
template <template-parameter-list> concept concept-name = constraint-expression;
下面定义了一个名为 Derived 的 Concept:
// concept template <class T, class U> concept Derived = std::is_base_of<U, T>::value;
下面定义了一个名为 Hashable 的 Concept:
#include <string>
#include <cstddef>
#include <concepts>
// Declaration of the concept "Hashable", which is satisfied by
// any type 'T' such that for values 'a' of type 'T',
// the expression std::hash<T>{}(a) compiles and its result is convertible to std::size_t
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
struct meow {};
template<Hashable T>
void f(T); // constrained C++20 function template
// Alternative ways to apply the same constraint:
// template<typename T>
// requires Hashable<T>
// void f(T);
//
// template<typename T>
// void f(T) requires Hashable<T>;
int main() {
f("abc"); // OK, std::string satisfies Hashable
f(meow{}); // Error: meow does not satisfy Hashable
}
2. Range
2.1. 动机
传统的 STL 算法往往把迭代器作为参数。比如对 std::vector v 进行排序。我们需要这样调用:
std::sort(v.begin(), v.end())
而不能像下面这样调用:
std::sort(v) // invalid
之所以这样设计的原因是前者会更加地通用。比如,我们可以仅对第 5 个元素后面的元素进行排序:
std::sort(v.begin() + 5, v.end())
不过,从使用者的角度来说, std::sort(v) 这种用法形式比起 std::sort(v.begin(), v.end()) 来说更加直观,也不容易出错。
C++20 中引入了 Range 的概念。A range is a group of items that you can iterator over.
命名空间 std::ranges:: 中提供了一些通用算法,如 std::ranges::sort 等等。当 v 是 Range 时( std::vector 是 Range), std::ranges::sort(v) 可以对 Range 中元素进行排序。
2.2. Range Concept
Range 或者是 input ranges(可以从中读取内容),或者是 output ranges(可以向其写入内容),或者是两者兼之。比如, std::vector<int> 即是 input range 又是 output range,而 std::vector<int> const 只是 input range。
对于 input range,有不同的 Concept 对其行为进行建模。如 1 表所示。
| Concept | Description |
|---|---|
| std::ranges::input_range | can be iterated from beginning to end at least once |
| std::ranges::forward_range | can be iterated from beginning to end multiple times |
| std::ranges::bidirectional_range | iterator can also move backwards with -- |
| std::ranges::random_access_range | you can jump to elements in constant-time [] |
| std::ranges::contiguous_range | elements are always stored consecutively in memory |
不同的容器满足不同的 Concept,如表 2 所示。
| Container | input_range | forward_range | bidirectional_range | random_access_range | contiguous_range |
|---|---|---|---|---|---|
| std::unordered_set | ✓ | ✓ | |||
| std::unordered_map | ✓ | ✓ | |||
| std::unordered_multiset | ✓ | ✓ | |||
| std::unordered_multimap | ✓ | ✓ | |||
| std::forward_list | ✓ | ✓ | |||
| std::set | ✓ | ✓ | ✓ | ||
| std::map | ✓ | ✓ | ✓ | ||
| std::multiset | ✓ | ✓ | ✓ | ||
| std::multimap | ✓ | ✓ | ✓ | ||
| std::list | ✓ | ✓ | ✓ | ||
| std::deque | ✓ | ✓ | ✓ | ✓ | |
| std::array | ✓ | ✓ | ✓ | ✓ | ✓ |
| std::vector | ✓ | ✓ | ✓ | ✓ | ✓ |
| std::string | ✓ | ✓ | ✓ | ✓ | ✓ |
2.3. Views
Views are ranges that are usually defined on another range and transform the underlying range via some algorithm or operation. Views do not own any data.
2.3.1. Lazy-evaluation
View 的一个重要特性是“惰性计算”,定义 View 时不进行计算,仅当真正读取 View 中数据时才计算。
#include <vector>
#include <ranges>
#include <iostream>
int main() {
std::vector vec{1, 2, 3, 4, 5, 6};
auto v = std::views::reverse(vec); // v 是一个 View,定义 v 时,不会进行“反转”操作
std::cout << *v.begin() << '\n'; // 当访问 v 时,才触发“反转”操作,这是惰性计算
return 0;
}
2.3.2. Combinability
可以对 View 多次转换,如:
#include <vector>
#include <ranges>
#include <iostream>
int main() {
std::vector vec{1, 2, 3, 4, 5, 6};
auto view1 = std::views::reverse(vec); // 6 5 4 3 2 1
auto view2 = std::views::filter([](int n){ return n % 2 == 0; })(view1); // 6 4 2
auto view3 = std::views::transform([](int n){ return n * 2; })(view2); // 12 8 4
for (auto v: view3) std::cout << v << " "; // 输出 12 8 4
return 0;
}
这种写法多次有些繁琐,我们可以改为下面更简单的形式:
#include <vector>
#include <ranges>
#include <iostream>
int main() {
std::vector vec{1, 2, 3, 4, 5, 6};
auto results = vec | std::views::reverse
| std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * 2; });
for (auto v: results) std::cout << v << " "; // 输出 12 8 4
return 0;
}
如果 r 满足 Concept viewable_range,下面形式:
r | foo | bar(2) | baz(5)
等价于:
baz(bar(foo(r), 2), 5)