Search code examples
c++generics

Is it possible to make algorithm handle object’s members generically?


I still not sure that the title is quite correct, since I don’t know the answer, so any suggestions to improve it are welcome.

I need a way to compute extents (min and max values) for some container of numbers while these containers could contain types which don't define operator< explicitly; so I need a way to consider their interval members as PoD to compare.

I have a template with handles extents of some value, let’s say:

template <typename T>
struct extents {
    // Initialized this "strange" way for further search min/max in containers
    T   min = std::numeric_limits<T>::max(),
        max = std::numeric_limits<T>::lowest();

    void update(T value) {
        min = std::min(min, value);
        max = std::max(max, value);
    }

    T midpoint() const {
        return std::midpoint(min, max);
    }
};

It works just fine with PoD types like here:

    float a[] = { 1.0f, 5.0f, 2.0f, 3.0f };

    extents<float> ext;

    for (auto v : a) {
        ext.update(v);
    }

(Well, I know std::minmax could be used here, sometimes it couldn’t).

With "extents" here I mean smallest and largest values of the container of elements.

When in comes to compound types I have to define operator< which is not trivial (like for my example with vectors). I definitely need something different than users (mathematicians, game developers, etc.) will expect here. I need to check if any of components is smaller than its counterpart in another compound value, so I don’t want and most often just can’t define operator< here. I can introduce separate methods, but again this is not always possible. Even if I do this, this would inflict performance impact, since I would be forced to handle vec2 as an object at most steps while I need member values only.

Is there a way to make code like here work?

    vec2 va[] = { {1.0f, 3.0f}, {5.0f, 2.0f} , {1.0f, 3.0f} };

    extents<vec2> vext;

    for (auto v : va) {
        vext.update(v);  // This won't compile because of reasons in text of the question
    }

I can do all this manually, the question is, can I somehow wrap this in extents class and its usage so that this is done automatically for any type compound from PoD types keeping the treatment of members to my responsibility or allowing me to control which members to take?

I would like to come with the solution like:

    vec2 va[] = { {1.0f, 3.0f}, {5.0f, 2.0f} , {1.0f, 3.0f} };

    extents<vec2> vext(&vec2::x, &vec2::y);

    for (auto v : va) {
        vext.update(v);
    }

which would mean “consider every member independently as a SOA (structure or arrays) or a channel when you compare them and as single unit AOS (array of structures) when you do assignment”.

In other words, I want my code to do the same as:

    vec2 va[] = { {1.0f, 3.0f}, {5.0f, 2.0f} , {1.0f, 3.0f} };

    extents<vec2>;

    for (auto v : va) {
        v.x.min = std::min(v.x.min,v);
        v.y.min = std::min(v.y.min,v);
        v.x.max = std::max(v.x.max,v);
        v.y.max = std::max(v.y.max,v);
    }

but without this mess of x,y,min/max in four lines where one update should be sufficient.

I assume it should work in a way

std::ranges::sort(va, std::less<>{}, &vec2::x);

works. I just need a way to specify the member to the update algorithm. The context seems to be very close: I need some algorithm to work based on some internal member.

The demo


Solution

  • Your code handles a single value, to make it handle a struct with many values you need to use a lot of variadic templates, this is possible in C++17 with fold expressions and std::apply, older C++ versions will have to do a lot more tricks.

    #include <limits>
    #include <numeric>
    #include <tuple>
    #include <iostream>
    #include <functional>
    
    template <typename T, typename...Getters>
    struct extents {
        std::tuple<Getters...> getters;
    
        T   min;
        T   max;
        extents(Getters&&...g)
            : getters{ std::move(g)... } {
            std::apply([&](auto&&...f) {
                ((std::invoke(f,min) = std::numeric_limits<std::decay_t<decltype(std::invoke(f,std::declval<T>()))>>::max()), ...);
                ((std::invoke(f,max) = std::numeric_limits<std::decay_t<decltype(std::invoke(f,std::declval<T>()))>>::lowest()), ...);
                }, getters);
        }
    
        void update(const T& value) {
            std::apply([&](auto&&...f)
                {
                    ((std::invoke(f,min) = std::min(std::invoke(f, min), std::invoke(f,value)),
                     (std::invoke(f,max) = std::max(std::invoke(f,max), std::invoke(f,value)))), ...);
                }, getters);
        }
    
        T midpoint() const {
            T t;
            std::apply([&](auto&&...f)
                {
                    ((std::invoke(f,t) = (std::invoke(f,min) + std::invoke(f,max)) / 2), ...);
                }, getters);
            return t;
        }
    };
    
    template <typename T, typename...Getters>
    auto make_extents(Getters&&...g) -> extents<T, Getters...>
    {
        return extents<T, Getters...>{std::forward<Getters>(g)...};
    }
    
    struct vec2 {
        float x = 0.0f, y = 0.0f;
    };
    
    int main()
    {
        vec2 va[] = { {1.0f, 3.0f}, {5.0f, 2.0f} , {1.0f, 3.0f} };
    
        auto vext = make_extents<vec2>(&vec2::x, &vec2::y);
    
        for (auto v : va) {
            vext.update(v);
        }
    
        std::cout << vext.midpoint().x << '\n';
        std::cout << vext.midpoint().y << '\n';
    }
    

    godbolt demo

    note that you need T to be default constructible, and you need to specify accessor functions (&vec::x, &vec::y), a more generic code is possible with either C++26 reflection or C++14 boost pfr, (in which case extents will not be templated on Getters and instead uses boost::pfr::get as getters)