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

Viewed 347

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.

2 Answers

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).

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>).

Related