查看原文
其他

C++23 | Ranges的修复与完善

cpluspluser CppMore 2023-04-20

C++23中,Ranges更新占比挺大,足有二十多个相关提案。

其中主要包含两部分内容,一是修复已知问题,二是完善遗落组件。简单来说,就是对于C++20的收尾工作。

本篇就集中于介绍Ranges的一些新变化。

1. Ranges转换为容器

C++20中,Ranges可以通过容器直接构造,而反过来却不行。

auto view =  std::views::iota(010) | std::views::common;
// std::vector<int> vec { view }; // ERROR!
std::vector<int> vec { std::ranges::begin(view), std::ranges::end(view) }; // OK

fmt::print("vec: {}\n", vec); // vec: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

甚至容器与容器之间也无法直接转换,只能这样使用:

std::list l { 123 };
// std::vector<decltype(l)::value_type> v { l }; // ERROR!
std::vector<decltype(l)::value_type> v { std::begin(l), std::end(l) }; // OK

fmt::print("v: {}\n", v); // v: [1, 2, 3]

而到了C++23,引入了ranges::to,可以方便地进行上述转换:

// views to container
auto view = views::iota(010);
std::vector<int> vec = view | ranges::to<std::vector>();
fmt::print("view to vector: {}\n", vec);

// container to container
std::list l { 123 };
std::vector<decltype(l)::value_type> v = l | ranges::to<std::vector>();
fmt::print("list to vector: {}\n", v);

// Output: 
// view to vector: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// list to vector: [1, 2, 3]


2. 格式化Ranges

这个是从其他语言那模仿来的,结合Formatting来格式化Ranges。

前面使用fmt输出容器其实就用的该特性,再来看个例子:

// Formatting ranges
std::string s = "x,y,x";
auto parts = s | views::split(',');

// [['x'], ['y'], ['x']]
fmt::print("{}\n", parts);

// <<['x']--['y']--['x']>>
fmt::print("<<{}>>\n", fmt::join(parts, "--"));

这里使用了split_view,它可以根据指定分割符将元素分离出来,通过fmt库可以很方便地打印结果。

关于C++23的IO新增特性过段时间会有专门的一篇进行介绍。

3. Ranges算法:starts_with和ends_with

C++20为string和string_view引入了starts_withends_with算法,使得字符串匹配操作更加简便。一个小例子:

constexpr std::string_view str = "START_FLAG12345END_FLAG";
constexpr bool start_with_flag = str.starts_with("START_FLAG");
constexpr bool end_with_flag   = str.ends_with("END_FLAG");

fmt::print("start_with_flag: {}\nend_with_flag:{}\n", start_with_flag, end_with_flag);

// Output:
// start_with_flag: true
// end_with_flag:true

到了C++23,Ranges也加入这两个算法,使得能够对所有的容器进行该操作。

同样一个小例子:

auto some_ints      = views::iota(050);
auto some_more_ints = views::iota(030);
if (ranges::starts_with(some_ints, some_more_ints)) {
    fmt::print("starts_with true\n"); 
}

// Output:
// starts_with true

4. 新Views:zip, zip_transform, adjacent和adjacent_transform

这四个Views,可以直接通过例子来理解:

std::vector v1 = { 12 };
std::vector v2 = { 'a''b''c' };
std::vector v3 = { 345 };

fmt::print("zip: {}\n"std::views::zip(v1, v2));
fmt::print("zip_transform: {}\n"std::views::zip_transform(std::multiplies(), v1, v3));
fmt::print("adjacent: {}\n", v2 | std::views::pairwise);
fmt::print("adjacent_transofrm: {}\n", v3 | std::views::pairwise_transform(std::plus()));

// Output:
// zip: {(1, 'a'), (2, 'b')}
// zip_transform: {3, 8}
// adjacent: {('a', 'b'), ('b', 'c')}
// adjacent_transofrm: {7, 9}

zip可以将两个Ranges压缩为一个Range(2个的时候元素类型为pairs,多个的时候元素类型为tuples),zip_transform会在压缩的时候执行转换操作。

ajacent是一种特殊的zip,它的输入只有一个Range,可以针对某几个元素进行分组,产生一个新的Range,而ajacent_transform则会在分组之后执行转换操作。

其中,pairwisepairwise_transform是ajacent_view和ajacent_transform_view按照两个单位分组时的别名。

因此,也可以这样使用:

vector v = { 1234 };

for (auto i : v | views::adjacent<2>) {
    // prints: (1, 2) (2, 3) (3, 4)
    cout << '(' << i.first << ', ' << i.second << ") ";
}

5. Ranges算法:fold

fold是数值算法std::accumulate更加通用的版本。

fold包含fold_leftfold_right两个算法,用法如下:

// fold algorithms                                                                                  
int xs[] = { 12345 };
auto concatl = [](std::string s, int i) { return s + std::to_string(i); };  
auto concatr = [](int i, std::string s) { return s + std::to_string(i); };

auto fold_left  = ranges::fold_left(xs, std::string(), concatl);
fmt::print("fold left: {}\n", fold_left);

auto fold_right = ranges::fold_right(xs, std::string(), concatr);
fmt::print("fold right: {}\n", fold_right);

// Output:
// fold left: 12345
// fold right: 54321

std::accumulate默认会执行累加操作,而fold表意更加广泛,所以加入到了<algorithm>之中。

6. Ranges算法:iota, shift_left和shift_right

尽管C++20已经有了iota_view,但是针对已有Ranges来说还是不太方便。

线性初始化一个数组,C++23之前有两种方法。

第一种方法是使用std::iota,该函数在C++20提供了constexpr版本,用法如下:

std::vector<int> vec(10);
std::iota(vec.begin(), vec.end(), 0);

fmt::print("{}\n", vec);

// Output:
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

第二种方法是使用std::views::iota,用法如下:

std::vector<int> vec;
for (int i : std::views::iota(010)) {
    vec.push_back(i);
}

fmt::print("{}\n", vec);

// Output: Ditto

到了C++23,算上ranges::to,又有了两种方法:

// 1. use ranges::to
std::vector<int> vec1 = views::iota(010) | to<std::vector>();
fmt::print("{}\n", vec1);

// 2. use ranges::iota
std::vector<int> vec2(10);
ranges::iota(vec2, 0);
fmt::print("{}\n", vec2);

// Output: Ditto

这些方法中,直接使用ranges::iota要更加便捷与高效。

shift_leftshift_right也是一组对称的算法,用法也比较简单,此处只展示一个:

std::vector v { 12345 };
auto it = ranges::shift_left(v, 2);
// v = 3 4 5 4 5
//           ^
//           it
v.erase(it, v.end());

// [3 4 5]
fmt::print("{}\n", v);

注释很清晰,不多解释。

7. Range adaptors: slide, chunk与chunk_by

这是几个新的Range adaptors,分别对应slide_view, chunk_viewchunk_by_view

先说前两个,简单的小例子:

std::vector v = {12345};

// [[1, 2], [3, 4], [5]]
fmt::print("{}\n", v | std::views::chunk(2));

// [[1, 2], [2, 3], [3, 4], [4, 5]]
fmt::print("{}\n", v | std::views::slide(2));

意义很明显,chunk用于对数据进行「分块」操作,slide用于对数据进行连续「分块」操作,和adjacent比较相似。区别在于它的参数发生于运行期,而adjacent是模板参数,发生于编译期。

chunk_by则可以根据某个条件进行「分块」,看个例子:

// chunk_by
std::vector v { 213-45 };

// [[2], [1, 3], [-4, 5]]
fmt::print("{}\n", v | views::chunk_by(std::less<>{}));

根据该adaptor,便可以根据任意条件打散并重排数据。

8. Range adaptor: views::join_with

这是一个与「分割」操作相反的「组合」操作Range adaptor,对应join_with_view

用法其实很简单:

vector<string> vs = {"the""quick""brown""fox"};
for (char c : vs | join_with('-')) {
    cout << c;
}

// Output:
// the-quick-brown-fox

可以按照指定方式将所有元素组合起来。

9. std::generator

这个是跟协程有关的,std::generator是一个move-only的view,模拟了input_range

因此可以对其使用views,比如:

std::generator<int> ints(int start = 0) {
    while (true)
        co_yield start++;
}

void f() {
    for (auto i : ints() | std::views::take(3))
    std::cout << i << ' '// prints 0 1 2
}

10. 修复istream_view的问题

istream_view在C++20存在一些基本的设计问题,在23进行了修复。

std::istringstream mystream { "0 1 2 3 4" };

// C++20 ERROR, C++23 OK
std::ranges::istream_view<int> v{ mystream };

11. Ranges算法:contains

在上一篇中介绍过ranges::find, ranges::search等等算法,要识别一个Range是否包含某个元素,或是否包含另一个子Range,比较麻烦。

C++23新引入了两个新算法:containscontains_subrange,可以很好的满足该需求。

int arr1[] = { 4231 };
int arr2[] = { 42 };
fmt::print("{}\n", contains(arr1, 4)); // true
fmt::print("{}\n", contains_subrange(arr1, arr2)); // true



Ranges的内容非常多,本篇基本覆盖了C++23新增的内容。

总的来说,Ranges的确方便了开发,以一种新的形式使用容器和算法,并配合Formatting库进行输出。对于字符串欠缺的一些操作也在Ranges算法中引入,这也解放了部分生产力。

但是,细节相当之多,两篇文章还不足以涉及到全部内容,后续还会间断着更一些相关文章,欢迎大家持续关注。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存