7

Consider a Point type with x, y and z values. If I have a range of Point objects, such as std::vector<Point>, what do I need to add to Point to make it work with the std::ranges::views::elements range adaptor?

The intention is to do something like

std::vector<Point> v{...};
for (auto x : v | std::ranges::views::elements<0>) {
    // do something with all `x` values
}

The documentation mentions that std::ranges::views::elements works with "tuple-like" values. I have assumed that it should work similarly to how we can make our type work with structured binding, but I seem to be missing something

I have tried with the following code

class Point {
    double x=0;
    double y=0;
    double z=0;
public:
    Point(double x, double y, double z) : x(x), y(y), z(z) {}

    template <std::size_t N>
    double get() const {
        if constexpr(N == 0)
            return x;
        else if constexpr(N == 1)
            return y;
        else if constexpr(N == 2)
            return z;
    }
};

namespace std {

template <>
struct tuple_size<Point> : std::integral_constant<std::size_t, 3> {};

template <std::size_t N>
struct tuple_element<N, Point> {
    using type = double;
};
}

This is enough to make structured binding work, but std::ranges::views::elements still does not work. Then I thought that maybe std::ranges::views::elements needed std::get<n>(p) to work and I added a specialization below to the std namespace

template <std::size_t N>
double get(const Point &p) {
    if constexpr(N == 0)
        return p.get<0>();
    else if constexpr(N==1)
        return p.get<1>();
    else if constexpr(N==2)
        return p.get<2>();
}

Now it is possible to use std::get<0>(p) to extract the x value, but again this is not enough for std::ranges::views::elements. What else is necessary to make a range of Point objects work with std::ranges::views::elements?


PS: I know I could just use a views::transform here, but my main intention here it to be generic and to understand how these things are suppose to fit together.generic and to understant how these things are supose to fit together.

darcamo
  • 3,294
  • 1
  • 16
  • 27

2 Answers2

4

Can can I make std::ranges::views::elements work with a range of my type

No, you can't.

The way that elements is specified, in [range.elements.view], it's constrained on:

  template<class T, size_t N>
  concept has-tuple-element =                   // exposition only
    requires(T t) {
      typename tuple_size<T>::type;
      requires N < tuple_size_v<T>;
      typename tuple_element_t<N, T>;
      { get<N>(t) } -> convertible_­to<const tuple_element_t<N, T>&>;
    };

but we have to keep in mind the general rule in the library, from [contents]/3 that:

Whenever a name x defined in the standard library is mentioned, the name x is assumed to be fully qualified as​ ::​std​::​x, unless explicitly described otherwise. For example, if the Effects: element for library function F is described as calling library function G, the function​ ::​std​::​G is meant.

The get<N>(t) there is not an unqualified call to get, it's a call to ::std::get<N>(t) (there is no "unless explicitly described otherwise").

What that means is that this is testing std::get<0> (for keys), and that's not going to find user-provided structured bindings support (which should either be a member e.get<0>() or an unqualified get<0>(e) in the associated namespace). You can't just add overloads into std to make this work.

So... not currently supported.


Technically, if you stick your overload of std::get<N>(Point) into namespace std and ensure that it is defined before <ranges> is included, this will Just WorkTM. But this is highly fragile, both because you have to carefully control the include order (which you can't really do) and involves adding overloads into std (which you also shouldn't do either, especially in this case where those overloads wouldn't help structured bindings anyway).

Barry
  • 286,269
  • 29
  • 621
  • 977
  • I thought of this, but it seemed so obviously useful a feature to omit that I assumed it to be a defect! I can’t find any discussion of the restriction, which seems to ultimately date back to [the second revision of P0789](http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0789r1.pdf) which lamentably contains absolutely no explanation of anything. – Davis Herring May 28 '21 at 17:33
  • @DavisHerring `elements` was added by P1035, but the issue here is that there is no library protocol for tuple-like... so it just only supports the library tuples. Plus ADL `get` is just... a problem. – Barry May 28 '21 at 18:53
  • I checked with the "fragile solution" and It works exactly as you mentioned. But indeed it is very fragile. For now I'll just use views::transform whenever I need it, instead of making my `Point` type work with the generic `views::elements`. – darcamo May 28 '21 at 19:29
0

The intent is that you provide a non-member get function template in your namespace (for ADL). This is more strict than structured bindings, which support member get as well.

Your specialization didn’t work because it’s not a specialization: it’s an overload and is thus disallowed in namespace std (and in practice wasn’t found by name lookup from within <ranges>).

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • I also tried with a non-member `get` function, but it didn't work. It seems that @Barry's answer is correct and the call to `get(Point)` is qualified with `std::`, which thus does not allow for ADL to find my non-member `get` implementation. – darcamo May 28 '21 at 19:18