How do I enable my type for std::ranges::to?
Asked Answered
K

3

5

How can you convert to a custom data structure using std::ranges::to?

Basically something like

struct my_data {
   char b;
   int foo;
   std::string baz;
};

auto text = "v 3 fool|x 56 description|e 123 name"sv;

auto converted = text | std::views::split('|') 
               | std::views::transform([](auto r) { 
                   return r | std::ranges::to<my_data>();
               }) | std::ranges::to<std::vector>();
// converted should be a vector<my_data>

What kind of thing does my_data need to have to be able to convert to it using std::ranges::to?

Kempe answered 18/12, 2023 at 14:30 Comment(0)
B
7

There are four possible constructors that r | std::ranges::to<C>{} will use. In order of preference (and ignoring perfect forwarding for simplicity):

  1. C(r) - pass the range object to the constructor.
  2. C(std::from_range, r) - as above, but explicitly tagged
  3. C(std::ranges::begin(r), std::ranges::end(r)) - iterator/sentinel pair.
  4. C c; c.reserve(std::ranges::size(r)) /* if supported */; std::ranges::copy(r, std::back_inserter(c)); - default-construct then copy.

It's probably easiest to create a constructor of the first kind that accepts a range:

my_data(std::ranges::input_range auto r);
Bloc answered 18/12, 2023 at 15:41 Comment(0)
R
4

You can provide a constructor for my_data that takes a range and requires that a std::string object can be constructed from that range, then uses the created std::string to construct a std::istringstream object to initialize each member:

struct my_data {
  char b;
  int foo;
  std::string baz;
  template<std::ranges::input_range R>
    requires std::constructible_from<std::string, std::from_range_t, R>
  my_data(R&& r) {
    std::string s(std::from_range, std::forward<R>(r));
    std::istringstream is(s);
    is >> b >> foo >> baz;
  }
};

Then you can slightly rewrite (simplify) the pipe as:

auto text = "v 3 fool|x 56 description|e 123 name"sv;

auto converted = text | std::views::split('|') 
                      | std::views::transform(std::ranges::to<my_data>())
                      | std::ranges::to<std::vector>();

Demo

As Barry's answer demonstrates, since splitting subranges models contiguous_range in your case, using std::ispanstream to extract the formatted data can be a more efficient option:

struct my_data {
  char b;
  int foo;
  std::string baz;
  my_data(std::span<const char> sp) {
    std::ispanstream is(sp);
    is >> b >> foo >> baz;
  }
};

Demo

Roanne answered 18/12, 2023 at 15:6 Comment(2)
Would've hoped for a direct conversion without the extra string step, but this works. Thanks.Kempe
@Kempe that's unrealistic. You'd need a reflection mechanism for that; the std version is not available. But there are adhoc 3rd party open-source libraries with lots of limitation on the input type.Piloting
E
3

The inner call to ranges::to here isn't really what the point of ranges::to is:

auto converted = text
               | std::views::split('|') 
               | std::views::transform([](auto r) { 
                   return r | std::ranges::to<my_data>(); // <== this one
               })
               | std::ranges::to<std::vector>();

It's true that, on some level, you're taking a range of char and converting it into my_data. But like - my_data isn't any kind of range, so it's kind of weird from a design perspective. The name of that function can really be... anything. Indeed you could just spell it:

auto converted = text
               | std::views::split('|') 
               | std::views::transform(stream_into<my_data>); // <== this one
               | std::ranges::to<std::vector>();

Providing a:

template <typename T>
inline constexpr auto stream_into = [](std::span<char const> s) -> T {
    T obj;
    std::ispanstream(s) >> obj;
    return obj;
};

And now you have a generic extracter, with a spelling that's more reflective of the actual operation being performed.


Either way, note that:

views::transform([](auto r){ return r | std::ranges::to<my_data>(); })

can be spelled as:

views::transform(std::ranges::to<my_data>())

This is the general property of range adaptor closure objects: R | C and C(R) are equivalent, so [](auto r) { return r | c; } has the same meaning as c.

Ellenaellender answered 19/12, 2023 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.