Ranges-v3中transform无法直接处理split_view结果的编译问题
问题场景
我尝试使用ranges-v3拆分SNMP OID字符串并返回std::deque<uint32_t>类型的拆分结果。
添加额外中转步骤后代码可正常运行,代码如下:
#include <range/v3/all.hpp> /// split the supplied string into nodes, using '.' as a delimiter /// @param the path to split , e.g "888.1.2.3.4" /// @return a std::deque<uint32_t> containing the split paths static std::deque<uint32_t> splitPath(std::string_view path) { constexpr std::string_view delim{"."}; auto tmp = path | ranges::views::split(delim) | ranges::to<std::vector<std::string>>() ; return tmp | ranges::views::transform([](std::string_view v) { return std::stoul(std::string{v}); }) | ranges::to<std::deque<uint32_t>>(); }
最初预期可以直接通过链式调用实现,无需中转变量:
static std::deque<uint32_t> splitPath(std::string_view path) { constexpr std::string_view delim{"."}; return path | ranges::views::split(delim) | ranges::views::transform([](std::string_view v) { return std::stoul(std::string{v}); }) | ranges::to<std::deque<uint32_t>>(); }
该写法触发如下编译错误:
error: no match for ‘operator|’ (operand types are ‘ranges::split_view<std::basic_string_view<char>, std::basic_string_view<char> >’ and ‘ranges::views::view_closure<ranges::detail:: bind_back_fn_<ranges::views::transform_base_fn, ahk::snmp:: {anonymous}::splitPath(std::string_view)::<lambda(std::string_view)> > >’) 36 | return path | ranges::views::split(delim) | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | | ranges::split_view<std::basic_string_view<char>, std::basic_string_view<char> > 37 | | ranges::views::transform([](std::string_view v) { | ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | | ranges::views::view_closure<ranges::detail::bind_back_fn_ <ranges::views::transform_base_fn, ahk::snmp:: {anonymous}::splitPath(std::string_view)::<lambda(std::string_view)> > > 38 | return std::stoul(std::string{v}); })
存在的疑问:
- 为什么调用
ranges::views::transform前必须先将第一步操作的结果转换为std::vector并存入命名变量tmp? - 如下移除命名变量
tmp的链式写法同样编译失败,原因是什么?
static std::deque<uint32_t> splitPath(std::string_view path) { constexpr std::string_view delim{"."}; return path | ranges::views::split(delim) | ranges::to<std::vector<std::string>>() | ranges::views::transform([](std::string_view v) { return std::stoul(std::string{v}); }) | ranges::to<std::deque<uint32_t>>(); }
需要明确问题根因,以及符合Ranges-v3设计规范的简洁正确实现方式。
问题根因
核心问题是对ranges::views::split返回的元素类型存在认知偏差:split执行后返回的是惰性拆分视图,遍历该视图得到的每个子元素不是std::string_view类型,而是ranges库内部定义的连续子范围类型,该类型无法隐式转换为std::string_view,因此直接传入接收std::string_view参数的lambda时,会出现管道运算符重载匹配失败,也就是你看到的编译错误。
中转写法能运行的原因是:你通过ranges::to<std::vector<std::string>>()提前把拆分视图实体化成了存储std::string的vector,存入命名变量tmp后,vector的元素是具名的std::string左值,可以隐式转换为std::string_view传入lambda,因此可以正常编译。
移除tmp的链式中转写法编译失败,是因为旧版本ranges-v3中,ranges::to返回的右值容器直接接入视图适配器时,存在右值引用绑定的适配问题,导致元素类型无法正确匹配lambda的参数类型。
另外需要注意:中途把视图实体化为std::vector<std::string>的写法属于反模式,会产生不必要的内存分配和字符串拷贝,完全违背ranges视图惰性求值、零拷贝的设计初衷。
符合规范的正确实现
不需要任何中间容器中转,直接在transform步骤中用子范围的迭代器构造std::string_view即可,全程零额外内存分配,写法简洁符合ranges设计规范:
#include <range/v3/all.hpp> #include <string_view> #include <deque> #include <cstdint> #include <string> /// split the supplied string into nodes, using '.' as a delimiter /// @param path the path to split , e.g "888.1.2.3.4" /// @return a std::deque<uint32_t> containing the split paths static std::deque<uint32_t> splitPath(std::string_view path) { constexpr std::string_view delim{"."}; return path | ranges::views::split(delim) | ranges::views::transform([](auto&& sub_range) { // 用子范围的首尾迭代器构造string_view,无额外拷贝 std::string_view part{sub_range.begin(), sub_range.end()}; return static_cast<uint32_t>(std::stoul(std::string{part})); }) | ranges::to<std::deque<uint32_t>>(); }
如果使用较新版本的ranges-v3,还可以直接用ranges::to<std::string_view>()直接把拆分的子范围转为std::string_view,写法更简洁:
static std::deque<uint32_t> splitPath(std::string_view path) { constexpr std::string_view delim{"."}; return path | ranges::views::split(delim) | ranges::views::transform([](auto&& sub_range) { auto part = sub_range | ranges::to<std::string_view>(); return static_cast<uint32_t>(std::stoul(std::string{part})); }) | ranges::to<std::deque<uint32_t>>(); }
内容的提问来源于stack exchange,提问作者mark

