C++23 | Ranges的修复与完善
C++23中,Ranges更新占比挺大,足有二十多个相关提案。
其中主要包含两部分内容,一是修复已知问题,二是完善遗落组件。简单来说,就是对于C++20的收尾工作。
本篇就集中于介绍Ranges的一些新变化。
1. Ranges转换为容器
C++20中,Ranges可以通过容器直接构造,而反过来却不行。
auto view = std::views::iota(0, 10) | 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 { 1, 2, 3 };
// 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(0, 10);
std::vector<int> vec = view | ranges::to<std::vector>();
fmt::print("view to vector: {}\n", vec);
// container to container
std::list l { 1, 2, 3 };
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_with和ends_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(0, 50);
auto some_more_ints = views::iota(0, 30);
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 = { 1, 2 };
std::vector v2 = { 'a', 'b', 'c' };
std::vector v3 = { 3, 4, 5 };
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则会在分组之后执行转换操作。
其中,pairwise和pairwise_transform是ajacent_view和ajacent_transform_view按照两个单位分组时的别名。
因此,也可以这样使用:
vector v = { 1, 2, 3, 4 };
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_left和fold_right两个算法,用法如下:
// fold algorithms
int xs[] = { 1, 2, 3, 4, 5 };
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,用法如下:
for (int i : std::views::iota(0, 10)) {
vec.push_back(i);
}
fmt::print("{}\n", vec);
// Output: Ditto
到了C++23,算上ranges::to,又有了两种方法:
// 1. use ranges::to
std::vector<int> vec1 = views::iota(0, 10) | 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_left与shift_right也是一组对称的算法,用法也比较简单,此处只展示一个:
std::vector v { 1, 2, 3, 4, 5 };
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_view与chunk_by_view。
先说前两个,简单的小例子:
std::vector v = {1, 2, 3, 4, 5};
// [[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 { 2, 1, 3, -4, 5 };
// [[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新引入了两个新算法:contains和contains_subrange,可以很好的满足该需求。
int arr1[] = { 4, 2, 3, 1 };
int arr2[] = { 4, 2 };
fmt::print("{}\n", contains(arr1, 4)); // true
fmt::print("{}\n", contains_subrange(arr1, arr2)); // true
Ranges的内容非常多,本篇基本覆盖了C++23新增的内容。
总的来说,Ranges的确方便了开发,以一种新的形式使用容器和算法,并配合Formatting库进行输出。对于字符串欠缺的一些操作也在Ranges算法中引入,这也解放了部分生产力。
但是,细节相当之多,两篇文章还不足以涉及到全部内容,后续还会间断着更一些相关文章,欢迎大家持续关注。