跳到主要内容

Range-v3,重塑现代C++的数据处理方式

  • Range-v3是一个现代C++库,它基于“范围”概念来提供强大的数据处理功能,使得操作和迭代集合更为直观和简洁。
  • 在C++11及以后的版本中,标准库逐步增加了对lambda表达式、自动类型推导等现代特性的支持,这极大地增强了语言的表达力和灵活性。然而,对于数据的处理尤其是集合的操作,传统的迭代器和算法虽然功能强大,但代码往往过于繁琐且难以阅读和维护。
  • Range-v3将“范围”概念引入C++中。它的目标是提供一种更为现代和高效的方式来操作数据,使得代码不仅更简洁,而且性能更优。
  • Ranges是标准模板库的扩展,它通过使迭代器和算法可组合,增强了其功能。与其他试图摒弃迭代器的类似范围解决方案不同,range-v3中的ranges是在迭代器之上的一个抽象层。
  • Range-v3建立在三个支柱上:视图(Views)、操作(Actions)和算法(Algorithms)。其中的算法与您在STL中已经熟悉的算法相同,只是在range-v3中,所有的算法都增加了接受范围的重载,除了接受迭代器的重载。视图是对范围的可组合适配,这种适配在迭代视图时惰性地发生。操作则是算法对容器的急切应用,它会就地修改容器并返回它以供进一步处理。

快速开始

  • Range-v3是一个通用库,它增强了现有标准库的功能,提供了处理范围的设施。范围可以大致被理解为一对迭代器的组合,尽管它们不一定要这样实现。将开始/结束迭代器捆绑成一个单一对象带来了几个好处:便利性、可组合性和正确性。

便利性

  • 将单个范围对象传递给算法比单独传递开始/结束迭代器更为方便。对比如下:
std::vector<int> v{/*...*/};
std::sort( v.begin(), v.end() );
std::vector<int> v{/*...*/};
ranges::sort( v );
  • Range-v3包含了所有标准算法的完整实现,并为便利性提供了基于范围的重载。

可组合性

  • 拥有单一的范围对象允许操作的流水线。在流水线中,范围通过视图被惰性适配或通过操作被急切地以某种方式变更,结果立即可用于进一步的适配或变更。惰性适配由视图处理,急切变更由操作处理。

  • 例如,下面的代码使用视图过滤容器中符合条件的元素,并使用函数转换得到的范围。注意,底层数据是常量,并且不会被视图改变。

std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){ return i % 2 == 1; })
| views::transform([](int i){ return std::to_string(i); });
// rng == {"2","4","6","8","10"};
  • 在上述代码中,rng 仅仅存储了对底层数据以及过滤和转换函数的引用。在迭代 rng 之前,不会执行任何操作。

  • 相比之下,操作(actions)急切地执行它们的工作,但它们也可以组合。考虑下面的代码,它将一些数据读入一个向量,对其进行排序并使其唯一化。

extern std::vector<int> read_data();
using namespace ranges;
std::vector<int> vi = read_data() | actions::sort | actions::unique;
  • 与视图不同,在管道中的每一步(actions::sortactions::unique)都通过值接受一个容器,就地修改它,并返回它。

正确性

  • 无论您是使用视图还是操作,您都是以纯函数式、声明式的风格操作数据。您很少需要自己处理迭代器,尽管在需要时它们存在于幕后。

  • 通过声明式和函数式操作,而不是命令式操作,我们减少了对明显状态操作及分支和循环的需要。这减少了程序可能处于的状态数量,从而降低了您的程序的错误率。

  • 简而言之,如果您能找到一种方式,通过对数据进行函数式转换的组合来表达您的解决方案,您可以通过构造使您的代码正确。

视图

  • 正如上文所述,范围相比迭代器的一个巨大优势是它们的可组合性。它们支持一种函数式编程风格,其中数据通过一系列组合器进行操作。此外,这些组合器可以是惰性的,只在请求答案时才工作,并且是纯函数式的,不会改变原始数据。这使得推理你的代码变得更容易。

  • 视图是一种轻量级的包装器,它以某种自定义的方式呈现底层元素序列的视图,而不改变或复制它。视图创建和复制成本低,并具有非拥有引用语义。下面是一些使用视图的例子:

  • 使用谓词过滤容器并转换它。

std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){return i % 2 == 1;})
| views::transform([](int i){return std::to_string(i);});
// rng == {"2","4","6","8","10"};
  • 生成一个从1开始的无限整数列表,对它们进行平方,取前10个,并对它们求和:
using namespace ranges;
int sum = accumulate(views::ints(1)
| views::transform([](int i){return i*i;})
| views::take(10), 0);
  • 使用范围理解在运行时生成一个序列,并用它初始化一个向量:
using namespace ranges;
auto vi =
views::for_each(views::ints(1, 10), [](int i) {
return yield_from(views::repeat_n(i, i));
})
| to<std::vector>();
// vi == {1,2,2,3,3,3,4,4,4,4,5,5,5,5,5,...}