C++20 引入了 ranges 库, 这是标准模板库 (STL) 的一个重大改进. ranges 库引入了一种新的方式来处理序列(如数组、向量等), 使代码更加简洁和表达性更强.

环境要求

  1. 编译器需要支持 C++23 标准.
  2. 安装有 libfmt(version 10.0.0 或更高版本), 用于输出格式化的数据.
使用在线编译器

如果在本地环境无法运行, 可以使用在线编译器 Compiler Explorer.
这里有一个已经配置好的样例, 替换代码即可运行.

Ranges/View 简介

Ranges(范围)

Ranges 是 C++20 引入的新标准, 用于统一处理不同类型的序列容器, 提供了更直观的操作方式.
与传统 STL 对比, STL 通过 std::beginstd::end 获取迭代器, 手动遍历序列.
Ranges 直接操作范围对象, 代码更加简洁.

代码示例:

#include <fmt/ranges.h>

#include <algorithm>
#include <ranges>
#include <vector>

int main() {
  std::vector<int> vec = {1, 3, 2, 9, 4};
  // before c++20
  std::sort(vec.begin(), vec.end());

  // with c++20
  std::ranges::sort(vec);

  return 0;
}

Views(视图)

C++ 的 Views 提供惰性计算, 能处理复杂的操作链.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};
  // 通过 Views 过滤和变换
  auto squared_evens = vec | std::views::filter([](int n) { return n % 2 == 0; })  // 过滤偶数
                       | std::views::transform([](int n) { return n * n; });       // 平方变换

  fmt::println("{}", squared_evens);
  return 0;
}

ranges/view 的特点

惰性计算

视图操作(如 filter 或 transform)不会立刻执行, 而是在数据实际访问时才触发计算, 避免不必要的开销.

轻量级

只保有原始数据的引用, 不会复制数据, 降低内存开销.

组合式操作

视图支持通过管道运算符 (|) 将多个操作串联起来, 如:

auto result = data | std::views::filter(condition) | std::views::transform(func);

减少代码冗长性, 提升可读性.

不可变性

视图操作不会直接修改原始数据, 只返回一个新视图, 保持原始数据不变.

使用样例

过滤操作

可以使用 filter 操作过滤序列中的元素,

#include <fmt/ranges.h>
#include <ranges>
#include <vector>

int main() {
  std::vector vec = {1, 2, 3, 4, 5, 6};
  auto even_view = vec | std::views::filter([](int n) { return n % 2 == 0; });

  fmt::println("even numbers: {}", even_view); // even numbers: [2, 4, 6]
}

变换操作

可以使用 transform 操作对每个元素应用一个函数.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector vec = {1, 2, 3, 4, 5};
  auto square_view = vec | std::views::transform([](int n) { return n * n; });

  fmt::println("square numbers: {}", square_view);  // square numbers: [1, 4, 9, 16, 25]
  return 0;
}

截取操作

  • take 操作可以从序列的开头截取指定数量的元素.
  • drop 操作可以丢弃指定数量的元素.
#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector vec = {1, 2, 3, 4, 5};
  auto first_three_view = vec | std::views::take(3);
  auto drop_three_view = vec | std::views::drop(3);

  fmt::println("take first three: {}", first_three_view);  // take first three: [1, 2, 3]
  fmt::println("drop first three: {}", drop_three_view);   // drop first three: [4, 5]
  return 0;
}

zip 操作 (C++23)

zip 操作可以将两个序列合并为一个元素对序列.
zip_transform 操作可以将两个序列的元素应用一个函数.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector v1{1, 2, 3};
  std::vector v2{'a', 'b', 'c'};
  auto r1{std::views::zip(v1, v2)};
  fmt::println("r1: {}", r1);  // [(1, 'a'), (2, 'b'), (3, 'c')]

  std::vector v3{4, 5, 6};
  auto r2{std::views::zip_transform(std::multiplies(), v1, v3)};
  fmt::println("r2: {}", r2);  // [4, 10, 18]
  return 0;
}

枚举操作(C++23)

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector vec = {'a', 'b', 'c', 'd', 'e'};
  auto r = vec | std::views::enumerate;
  for (auto [i, c] : r) {
    fmt::println("index: {}, value: {}", i, c);
  }
  // output:
  // index: 0, value: a
  // index: 1, value: b
  // index: 2, value: c
  // index: 3, value: d
  // index: 4, value: e
}

访问键值对

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector<std::pair<int, std::string>> vec = {{1, "Ken"}, {2, "Ben"}, {3, "Alan"}};
  auto keys = vec | std::views::keys;
  auto values = vec | std::views::values;

  fmt::println("keys: {}", keys);      // keys: [1, 2, 3]
  fmt::println("values: {}", values);  // values: ["Ken", "Ben", "Alan"]
  return 0;
}

访问 tuple 的元素:

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector<std::tuple<int, std::string, std::string>> vec = {
      {1, "Ken", "Briton"}, {2, "Ben", "USA"}, {3, "Alan", "Canada"}};
  auto addr = vec | std::views::elements<2>;
  fmt::println("addr: {}", addr);  // addr: [Briton, USA, Canada]
}

分割操作

使用split操作可以将字符串分割为子字符串.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::string str = "Hello, World!";
  auto r15 = str | std::views::split(',');
  fmt::println("{}", r15);  // [['H', 'e', 'l', 'l', 'o'],
                            // [' ', 'W', 'o', 'r', 'l', 'd', '!']]
}

合并操作

使用join操作可以将子字符串合并为一个字符串.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>
#include <string>

using namespace std::string_literals;  // from C++14

int main() {
  std::vector vs{"hi"s, "you"s, "!"s};
  auto s1{vs | std::views::join};
  fmt::println("{}", s1);  // ['h', 'i', 'y', 'o', 'u', '!']

  auto s2{vs | std::views::join_with('\n')}; // from C++23
  fmt::println("{}", s2);  // ['h', 'i', '\n', 'y', 'o', 'u', '\n', '!']
}

滑动窗口操作

slide 操作可以生成一个滑动窗口序列.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector v{1, 2, 3, 4, 5};
  auto r7{v | std::views::slide(2)}; // from C++23
  fmt::println("r7: {}", r7);  // [[1, 2], [2, 3], [3, 4], [4, 5]]
}

adjacent操作与此有点像, 可以生成一个相邻元素对序列.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector v{1, 2, 3, 4};
  auto r3{v | std::views::adjacent<2>}; // from C++23
  fmt::println("r3: {}", r3);  // [(1, 2), (2, 3), (3, 4)]
}

分块访问

chunk 操作可以将序列分块. 注意 chunk 之间没有重叠.

{
  std::vector v{1, 2, 3, 4, 5};
  auto r8{v | std::views::chunk(2)};
  fmt::println("r8: {}", r8);  // [[1, 2], [3, 4], [5]]
}

{
  std::vector v{1, 2, 2, 3, 0, 4, 5, 2};
  auto r9{v | std::views::chunk_by(std::ranges::less_equal{})}; // from C++23
  // explain:
  // 1 < 2 <= 2 < 3;
  // 0 < 4 <= 5;
  // 2
  fmt::println("r9: {}", r9);  // {(1,2,2,3),(0,4,5),(2)}
}

间隔访问

stride 操作可以按照指定的步长访问序列.

#include <fmt/ranges.h>

#include <ranges>
#include <vector>

int main() {
  std::vector v{1, 2, 3, 4, 5};
  auto r{v | std::views::stride(2)}; // from C++23
  fmt::println("{}", r);  // {1, 3, 5}
}

结论和总结

Ranges 和 Views 是 C++20 标准的重要组成部分, 为序列处理提供了更高层的抽象和更高效的实现. 它们通过统一的接口减少了对迭代器的直接依赖, 并通过惰性计算显著优化了性能. 尽管 ranges 提供了强大的功能, 但仍需注意其对编译器支持的要求(如 C++20 以上)以及一些尚未完全覆盖的边界情况.

参考链接

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐