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)