c++functional-programmingstd-rangesc++23

Why is it done in std::ranges that I can't split the range that I got from join with transform


Why is it done in std::ranges that I can't split the range that I got from join with transform

Why doesn't this code compile and how can I fix it while still using the declarative approach with std::ranges and not creating intermediate not lazy containers as std::vector after transform ?

auto Foo(std::vector<std::string_view> viewsVec) -> void {
  std::println("{}",
               viewsVec | std::views::transform([](std::string_view str) -> std::string_view {
                 return str.substr(1);
               }) | std::views::join |
                   std::views::split('\n'));
}

I get the following compilation error: clang++ -std=c++23 split.cpp

split.cpp:10:38: error: invalid operands to binary expression ('invoke_result_t<const __fn &, transform_view<ref_view<vector<string_view,
      allocator<string_view>>>, (lambda at split.cpp:8:49)>>' (aka
      'std::ranges::join_view<std::ranges::transform_view<std::ranges::ref_view<std::vector<std::string_view>>, (lambda at split.cpp:8:49)>>') and
      '__range_adaptor_closure_t<__bind_back_t<__fn, tuple<char>>>' (aka
      'std::ranges::__range_adaptor_closure_t<std::__bind_back_t<std::ranges::views::__split_view::__fn, std::tuple<char>>>'))
    8 |                viewsVec | std::views::transform([](std::string_view str) -> std::string_view {
      |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    9 |                  return str.substr(1);
      |                  ~~~~~~~~~~~~~~~~~~~~~
   10 |                }) | std::views::join |
      |                ~~~~~~~~~~~~~~~~~~~~~ ^
   11 |                    std::views::split('\n'));
      |                    ~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/v1/cstddef:73:45: note: candidate function not viable: no known conversion from 'invoke_result_t<const __fn &,
      transform_view<ref_view<vector<string_view, allocator<string_view>>>, (lambda at split.cpp:8:49)>>' (aka
      'std::ranges::join_view<std::ranges::transform_view<std::ranges::ref_view<std::vector<std::string_view>>, (lambda at split.cpp:8:49)>>') to 'byte' for 1st
      argument
   73 | _LIBCPP_HIDE_FROM_ABI inline constexpr byte operator|(byte __lhs, byte __rhs) noexcept {
      |                                             ^         ~~~~~~~~~~
/usr/include/c++/v1/__charconv/chars_format.h:34:53: note: candidate function not viable: no known conversion from 'invoke_result_t<const __fn &,
      transform_view<ref_view<vector<string_view, allocator<string_view>>>, (lambda at split.cpp:8:49)>>' (aka
      'std::ranges::join_view<std::ranges::transform_view<std::ranges::ref_view<std::vector<std::string_view>>, (lambda at split.cpp:8:49)>>') to 'chars_format' for
      1st argument
   34 | inline _LIBCPP_HIDE_FROM_ABI constexpr chars_format operator|(chars_format __x, chars_format __y) {
      |                                                     ^         ~~~~~~~~~~~~~~~~
/usr/include/c++/v1/__ranges/range_adaptor.h:71:1: note: candidate template ignored: constraints not satisfied [with _Range =
      invoke_result_t<const __fn &, transform_view<ref_view<vector<string_view, allocator<string_view>>>, (lambda at split.cpp:8:49)>>, _Closure =
      __range_adaptor_closure_t<__bind_back_t<__fn, tuple<char>>>]
   71 | operator|(_Range&& __range, _Closure&& __closure) noexcept(is_nothrow_invocable_v<_Closure, _Range>) {
      | ^
/usr/include/c++/v1/__ranges/range_adaptor.h:69:12: note: because
      'invocable<std::ranges::__range_adaptor_closure_t<std::__bind_back_t<std::ranges::views::__split_view::__fn, std::tuple<char> > >,
      std::ranges::join_view<std::ranges::transform_view<std::ranges::ref_view<std::vector<std::string_view> >, (lambda at split.cpp:8:49)> > >' evaluated to false
   69 |   requires invocable<_Closure, _Range>
      |            ^
/usr/include/c++/v1/__concepts/invocable.h:28:3: note: because 'std::invoke(std::forward<_Fn>(__fn), std::forward<_Args>(__args)...)' would be
      invalid: no matching function for call to 'invoke'
   28 |   std::invoke(std::forward<_Fn>(__fn), std::forward<_Args>(__args)...); // not required to be equality preserving
      |   ^
/usr/include/c++/v1/__ranges/range_adaptor.h:77:52: note: candidate template ignored: constraints not satisfied [with _Closure =
      invoke_result_t<const __fn &, transform_view<ref_view<vector<string_view, allocator<string_view>>>, (lambda at split.cpp:8:49)>>, _OtherClosure =
      __range_adaptor_closure_t<__bind_back_t<__fn, tuple<char>>>]
   77 | [[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr auto operator|(_Closure&& __c1, _OtherClosure&& __c2) noexcept(
      |                                                    ^
/usr/include/c++/v1/__ranges/range_adaptor.h:75:11: note: because
      'std::ranges::join_view<std::ranges::transform_view<std::ranges::ref_view<std::vector<std::string_view>>, (lambda at split.cpp:8:49)>>' does not satisfy
      '_RangeAdaptorClosure'
   75 | template <_RangeAdaptorClosure _Closure, _RangeAdaptorClosure _OtherClosure>
      |           ^
/usr/include/c++/v1/__ranges/range_adaptor.h:62:32: note: because
      '!ranges::range<remove_cvref_t<join_view<transform_view<ref_view<vector<string_view, allocator<string_view> > >, (lambda at split.cpp:8:49)> > > >' evaluated to
      false
   62 | concept _RangeAdaptorClosure = !ranges::range<remove_cvref_t<_Tp>> && requires {
      |                                ^
1 error generated.

clang --version:

clang version 19.1.0+libcxx
Target: x86_64-pc-linux-musl
Thread model: posix
InstalledDir: /usr/lib/llvm/19/bin
Configuration file: /etc/clang/x86_64-pc-linux-musl-clang.cfg

Solution

  • Reformatting your code a bit and dropping the println which isn't relevant:

    auto Foo(std::vector<std::string_view> viewsVec) -> void {
      auto f = viewsVec // (1)
             | std::views::transform([](std::string_view str) -> std::string_view {
                    return str.substr(1);
               })       // (2)
            | std::views::join // (3)
            | std::views::split('\n'); // (4)
    }
    

    We can annotate our steps:

    1. viewsVec is a contiguous range of string_view&
    2. the transform is a random access range of string_view
    3. the join is an input range of char const&
    4. which means this doesn't work because views::split requires forward or better.

    joining a range of prvalue ranges has to be an input range because each range has to be stored somewhere — which has to be within the join_view itself. And once you're caching you can't be anything but input.

    And then split requires forward in order to give convenient behavior and be a range of subrange — which is only possible if you can actually have multiple iterators into your range.

    For this, you need views::lazy_split — which is a less convenient range adapter, but works on input ranges.