Search code examples
c++boostr-tree

How to properly use indexable in Boost?


In this example, I wrote two methods for allowing rtree.query to work:

  1. A Specialization of indexable for the Entity<float>
  2. A method wrapper

Which one would be better in this case?

#include <SFML/Graphics.hpp>
#include <iostream>
#include <sstream>
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/index/rtree.hpp>
#include <random>

#define METHOD_WRAPPER 0
#define SPECIALIZATION 1
#define SOLUTION METHOD_WRAPPER

using point = boost::geometry::model::d2::point_xy<float>;

template<typename T, typename IndexableGetter=boost::geometry::index::indexable<T>>
class Collection {
public:
    using box = boost::geometry::model::box<point>;
    void insert(T const& t) { rtree.insert(t); }
    auto begin() const { return rtree.begin(); }
    auto end() const { return rtree.end(); }
    auto find(auto const& search, float radius) {
        std::vector<std::reference_wrapper<T const>> result;
        rtree.query(boost::geometry::index::satisfies([&](auto const& el) {
            return boost::geometry::distance(el.position, search) < radius ;
        }), std::back_inserter(result));
        return result;
    }

private:
    boost::geometry::index::rtree<T, boost::geometry::index::quadratic<32>, IndexableGetter> rtree;
};


template<typename T>
struct Entity {
    using result_type = point;
    point position;
    Entity(T x, T y) : position(x, y) {}
#if SOLUTION == METHOD_WRAPPER
    struct ByPosition {
        using result_type = point;
        result_type const& operator()(Entity const& boid) const { return boid.position; }
    };
#endif
    operator sf::Vector2f() const { return sf::Vector2f(position.x(), position.y()); }
};

#if SOLUTION == SPECIALIZATION
namespace boost { namespace geometry { namespace index {

template<>
struct indexable<Entity<float>> {
    using result_type = point;
    result_type operator()(Entity<float> const& e) const {
        return result_type(e.position);
    }
};

}}}
#endif

int main() {
    using Entity = Entity<float>;

    #if SOLUTION == METHOD_WRAPPER
    Collection<Entity, Entity::ByPosition> collection;
    #elif SOLUTION == SPECIALIZATION
    Collection<Entity> collection;
    #endif

    for (int i = 0; i < 10; i++)
        collection.insert(Entity(
            static_cast<float>(rand() % 100),
            static_cast<float>(rand() % 100))
        );

    for (auto& el : collection)
        std::cout << el.position.x() << ", " << el.position.y() << std::endl;

    auto result = collection.find(point(50, 50), 30);
    for (auto& el : result)
        std::cout << el.get().position.x() << ", " << el.get().position.y() << std::endl;
}

Solution

  • In your case they are (obviously?) exactly equivalent. The indexable trait merely makes the indexable-getter the default for the element type.

    You can make that more obvious by removing the duplication:

    template <typename T> struct bgi::indexable<Entity<T>> : Entity<T>::ByPosition {};
    

    This improves on your specialization by being generic for all T instead of hardcoding T = float. Using a simplified version of your code to demonstrate side-by-side:

    Live On Coliru

    #include <boost/geometry.hpp>
    #include <boost/geometry/geometries/point_xy.hpp>
    #include <boost/geometry/index/rtree.hpp>
    #include <iostream>
    #include <random>
    
    namespace bg  = boost::geometry;
    namespace bgi = bg::index;
    using point   = bg::model::d2::point_xy<float>;
    
    template <typename T, typename IndexableGetter = bgi::indexable<T>> class Collection {
      public:
        using value_type = T;
    
        void insert(T const& t) { rtree.insert(t);      }
        auto begin() const      { return rtree.begin(); } 
        auto end() const        { return rtree.end();   } 
    
        auto find(auto const& search, float radius) {
            std::vector<std::reference_wrapper<T const>> result;
            rtree.query(
                bgi::satisfies([&](auto const& el) { return bg::distance(el.position, search) < radius; }),
                back_inserter(result));
            return result;
        }
    
      private:
        // using box = bg::model::box<point>;
        bgi::rtree<T, bgi::quadratic<32>, IndexableGetter> rtree;
    };
    
    template <typename T> struct Entity {
        point position;
        Entity(T x, T y) : position(x, y) {}
    
        struct ByPosition {
            using result_type = point;
            constexpr result_type const& operator()(Entity const& boid) const { return boid.position; }
        };
        // operator sf::Vector2f() const { return sf::Vector2f(position.x(), position.y()); }
    };
    
    template <typename T> struct bgi::indexable<Entity<T>> : Entity<T>::ByPosition {};
    
    template <typename Coll> void foo(int seed) {
        std::cout << "\n" << __PRETTY_FUNCTION__ << "\n";
        using Entity = Coll::value_type;
    
        auto dist = bind(std::uniform_int_distribution(0, 99), std::mt19937(seed));
        auto gen  = [&] { return Entity(dist(), dist()); };
        auto dump = [](auto caption, auto const& ee) {
            std::cout << " -- " << caption << ":\t";
            for (auto sep = " "; Entity const& el : ee)
                std::cout << std::exchange(sep, ", ") << bg::dsv(el.position);
            std::cout << "\n";
        };
    
        Coll coll;
    
        for (int i = 0; i < 5; i++)
            coll.insert(gen());
    
        dump("coll", coll);
        dump("result", coll.find(point(50, 50), 30));
    
        for (int i = 0; i < 5; i++)
            coll.insert(gen());
    
        dump("coll", coll);
    }
    
    int main() {
        using Entity = Entity<float>;
    
        foo<Collection<Entity, Entity::ByPosition>>(42);
        foo<Collection<Entity>>(42);
    }
    

    Printing e.g.

    void foo(int) [with Coll = Collection<Entity<float>, Entity<float>::ByPosition>]
     -- coll:    (79, 37), (18, 95), (77, 73), (59, 59), (44, 15)
     -- result:  (59, 59)
     -- coll:    (79, 37), (18, 95), (77, 73), (59, 59), (44, 15), (9, 15), (45, 5), (33, 86), (14, 60), (65, 70)
    
    void foo(int) [with Coll = Collection<Entity<float> >]
     -- coll:    (79, 37), (18, 95), (77, 73), (59, 59), (44, 15)
     -- result:  (59, 59)
     -- coll:    (79, 37), (18, 95), (77, 73), (59, 59), (44, 15), (9, 15), (45, 5), (33, 86), (14, 60), (65, 70)
    

    Benefits Of Either Approach

    The trait makes it easier to default, so if you have generic code that should be able to figure out a suitable indexablegetter, that's your ticket. You might also want to be able to reuse the getter for any entity template instantiation: Live On Coliru:

    struct GetPosition {
        using result_type = point;
        template <typename T> point const& operator()(Entity<T> const& e) const { return e.position; }
        template <typename T> point const& operator()(Resource<T> const& r) const { return r.position; }
    };
    
    template <typename T> struct bgi::indexable<Entity<T>> : GetPosition {};
    

    That said, if you want to express the intent that the user of Collection needs to choose between different getters available, it may be best to not default to a single getter. See also: The Pit Of Success design guide.