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>