C++23 ranges::to:范围转换函数 (P1206R7)

发布于:2025-05-15 ⋅ 阅读:(10) ⋅ 点赞:(0)

引言

在C++的发展历程中,每一个新版本都会带来一系列令人期待的新特性,C++23也不例外。其中,ranges::to 范围转换函数(P1206R7)的引入,为数据转换提供了更加便捷、高效的解决方案。本文将深入探讨 ranges::to 的定义、功能、使用场景以及其在实际编程中的优势。

C++23 Ranges 概述

在了解 ranges::to 之前,我们先来简单回顾一下C++23中Ranges的相关内容。C++23中,Ranges更新占比较大,主要包含两部分内容:一是修复已知问题,二是完善遗落组件,可看作是对C++20的收尾工作。

Ranges提供了一种抽象,允许C++程序对数据结构的元素进行统一操作,我们可以把它看作是对两个迭代器的泛化。最简单的范围应该定义了 begin()end() 元素。有几种不同类型的范围,如容器(containers)、视图(views)、大小范围(sized ranges)、假借范围(borrowed ranges)、双向范围(bidirectional ranges)、向前范围(forward ranges)等等。

ranges::to 的定义与功能

定义

ranges::to 是C++23引入的一个非常强大的功能,用于从范围构造对象(通常是容器)。其定义如下([range.utility.conv.to]):

template<class C, input_range R, class... Args> requires (!view<C>)
  constexpr C to(R&& r, Args&&... args);

需要注意的是,它仅限制模板参数 C 不是一个 view,也就是说,C 甚至可能不是一个 range。然而,它的实现用于 range_value_t<C> 获取 C 的元素类型,这使得 C 至少需要对 range 进行建模。

功能

ranges::to 的主要功能是将某个范围收集到某个对象中。它可以通过调用一个接受范围的构造函数、一个 std::from_range_t 标记的范围构造函数、一个接受迭代器 - 哨兵对的构造函数,或者通过将源范围的每个元素反向插入到参数构造的对象中来从源范围构造一个新的非视图对象。

使用场景

范围转换为容器

在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

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

// views to container
auto view = views::iota(0, 10);
std::vector<int> vec = view | ranges::to<std::vector>;

同样,容器与容器之间也可以直接转换:

std::list l { 1, 2, 3 };
std::vector<decltype(l)::value_type> v = l | ranges::to<std::vector>;

简化字符串解析

在解析简单分隔字符串时,ranges::to 也能发挥很大的作用。在C++20中,解析分隔字符串可能需要编写一些额外的代码:

const std::string& n = "1,2,3,4";
const std::string& delim = ",";
std::vector<std::string> line;
for (const auto& word : std::views::split(n, delim)) {
    line.push_back(std::string(word.begin(), word.end()));
}

随着C++23 ranges::to 的引入,这可以写成:

const std::string& n = "1,2,3,4";
const std::string& delim = ",";
const auto line = n | std::views::split(delim)
                  | std::ranges::to<std::vector<std::string_view>>();

映射转换为向量

在C++23中,我们可以将 views::keysranges::to 结合起来,将映射转换为它的值类型的向量:

template<typename MapT>
auto mapToVec(const MapT &_map) {
  return _map | std::views::values | std::ranges::to<std::vector>();
}

ranges::to 的优势

代码简洁性

ranges::to 的出现,使得代码更加简洁明了。在以往的数据转换过程中,可能需要编写大量的代码来完成,而现在只需要使用 ranges::to 就可以轻松实现。例如,将范围转换为容器的操作,使用 ranges::to 可以减少不必要的代码,提高代码的可读性和可维护性。

提高开发效率

在实际编程中,我们常常需要将数据从一种形式转换为另一种形式,ranges::to 就像是为我们提供了一辆高效的运输车,大大提高了开发效率。它减少了开发者手动编写转换代码的工作量,让开发者可以将更多的精力放在业务逻辑的实现上。

与C++23的stl容器的范围版本构造函数配合良好

std::ranges::to 与C++23的stl容器的范围版本构造函数配合良好,使得数据转换更加流畅。它能够更好地适应C++23的新特性,为开发者提供了更加统一和高效的编程体验。

模板参数约束的思考

ranges::to 的定义中,对模板参数 C 的约束较为宽松,只限制其不是 view。论文的R3版本过去常常约束 Cinput_range,但在R4中这个约束被删除了。

ranges::to 的目标是将某个范围收集到某物中,但它不一定是实际范围,只是消耗所有元素的东西。例如,假设我们有一个范围 std::expected<int, std::exception_ptr>,称之为 results。我们可以将其收集到一个 std::vector<std::expected<int, std::exception_ptr>> 中,也可以将其收集到 std::expected<std::vector<int>, std::exception_ptr> 中:

auto processed = results | ranges::to<std::expected>();
if (not processed) {
    std::rethrow_exception(processed.error());
}

std::vector<int> values = std::move(processed).value();
// go do more stuff

这种约束放松的好处在于,它支持了更多有趣的用例,并且不支持这些用例并不需要付出任何代价,我们只需要不要过早地拒绝它。

总结

C++23的 ranges::to 范围转换函数(P1206R7)为数据转换带来了极大的便利。它简化了代码,提高了开发效率,并且与C++23的stl容器的范围版本构造函数配合良好。同时,其对模板参数约束的设计也为更多有趣的用例提供了支持。随着C++23的逐渐普及,ranges::to 将会在实际编程中发挥越来越重要的作用。开发者可以充分利用这一特性,让自己的代码更加简洁、高效。