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 表所示。

Table 1: Input Range Concept
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 所示。

Table 2: Container and Concept
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)

Author: cig01

Created: <2020-03-29 Sun>

Last updated: <2020-06-27 Sat>

Creator: Emacs 27.1 (Org mode 9.4)