#UnorderedTuple - a static associative container

8 messages · Page 1 of 1 (latest)

frank granite
#

This is the first "thing" I've made in C++, so writing the code is mostly just an execercise. If you things I'm doing that are just bad, let it rip - I want to know.
I don't think this is the best solution for most problems - but I think it's maybe the right solution for this problem. What do y'all think:

A while back, I was using a C# SDK for a database connection. When you execute a query, it gives you back a row object that basically is a dictionary accessed by the string-name of the fields. row["plant"] gave you the 'plant' attribute at the row cursor. Since the row used a string as a key, there's no way to statically check for issues like typos. This lead to a lot of errors. I remember one typo with a space at the end: row["plant "] took me a whole day to find. Now, I'm (sort-of) making an entity-component-system framework that will have similar use - The UnordredTuple is my solution to this problem.

The UnorderedTuple is a static associative table. It's supposed to feel like a structure or tuple but have better semantics for transforming them. An UnorderedTuple<Name, Age, Address> is guaranteed to have a field for a name, age and address. Name, Age and Address are also the types of the fields name and and address respectively.
Since the field name is also the return type and key for the table, you can't have duplicates. The intention is that if Name is an integer, you can typedef(int, Name). If Name and Age are integers, there will be a collision, but boost::serialization has a STRONG_TYPEDEF macro that creates distinct types.

Unsurprisingly, the UnorderedTuple uses a tuple. But, you can add to, remove from, and permute the elements in the UnorderedTuple. UnorderedTuple<A, B, C> can be used interchangeably with UnorderedTuple<C, A, B>. Casting is free, you could just cast a pointer to it and it would behave identically.

#

You create them like this:

auto t1 = UnorderedTuple<A, B, C> {1, 2, 3};  // A, B, C, D... are integral types

You can add elements:

auto t1 = UnorderedTuple<A, B, C, D> (t1, 4);
// Or 

auto t1 = UnorderedTuple<A, B, C, D> (t1, D(4));

and remove elements:

// or
auto t2 = UnorderedTuple<A, B>(t)

I'm working on getting more readable formats like this:

UnorderedTuple<A, B, C, D> t1 = t.add<D>();
UnorderedTuple<A, B> t2 = t.remove<C>();
#
#include "tuple"
#include "any"
#include "boost/mp11.hpp"
#include "metautil.h"

    using namespace boost::mp11;

    template<typename ... Ts>
    class UnorderedTuple {
    private:
        // Types must be unique
        using order_before = mp_list<Ts...>;
        static_assert(mp_is_set<order_before>::value, "Types are not unique");

        // Sort <Ts..., Is...> by descending sizeof(T), then nameof(T)
        template<typename T, typename U>
        using Compare = mp_bool<(sizeof(T) > sizeof(U))
                                || ((sizeof(T) == sizeof(U)) && (constexpr_hash<T> < constexpr_hash<U>))>;
        using sorted_types = typename Sort<Compare, Ts...>::sorted_types;

    public:
        using OrderedTuple = mp_rename<sorted_types, tuple>;
        OrderedTuple tuple;

        explicit constexpr UnorderedTuple(Ts ... args) {
            (..., [&]
            {
                std::get<Ts>(tuple) = args;
            }());
        };

        virtual ~UnorderedTuple() = default;

        // Cast between: Ts... is a permutation of Us...
        template<typename ... Perm>
            requires(is_same_v<OrderedTuple, typename UnorderedTuple<Perm...>::OrderedTuple>)
        explicit UnorderedTuple(const UnorderedTuple<Perm...>& other) : tuple(other.tuple) {}

        // Cast down: Ts... is a subset of Us...
        template<typename ... Superset>
            requires(is_proper_subset<mp_list<Ts...>, mp_list<Superset...>>::value)
        explicit UnorderedTuple(const UnorderedTuple<Superset...>& superset) {
            (..., [&]
            {
                std::get<Ts>(tuple) = std::get<Ts>(superset.tuple);
            }());
        }
#

        // Cast up: Ts... is a superset of Us...
        template<typename ... Subset, typename ... Rest>
            requires(is_proper_subset<mp_list<Subset...>, mp_list<Ts...>>::value)
        explicit UnorderedTuple(const UnorderedTuple<Subset...>& subset, Rest... rest) {
            // Assign values from other tuple (subset)
            (..., [&]
            {
                std::get<Subset>(tuple) = std::get<Subset>(subset.tuple);
            }());
            // Assign additional values
            (..., [&]
            {
                std::get<Rest>(tuple) = rest;
            }());
        }

        template<typename T> inline constexpr auto
        get() const -> decltype(auto){
            return std::get<T>(tuple);
        }
        template<typename T> inline auto
        get() -> decltype(auto){
            return std::get<T>(tuple);
        }

        template<typename ... Us>
        constexpr bool operator==(
                const UnorderedTuple<Us...>& them) noexcept {
            return tuple == them.tuple;
        }
    };

    template<typename T, typename ... Ts> inline constexpr auto
    get(const UnorderedTuple<Ts...>& unordered_tuple) -> decltype(auto){
        return std::get<T>(unordered_tuple.tuple);
    }
    template<typename T, typename ... Ts> inline auto
    get(UnorderedTuple<Ts...>& unordered_tuple) -> decltype(auto){
        return std::get<T>(unordered_tuple.tuple);
    }

    template<typename ... Ts, typename ... Us>
    static constexpr bool operator==(
            const UnorderedTuple<Ts...>& tuple1,
            const UnorderedTuple<Us...>& tuple2) noexcept {
        return tuple1.tuple == tuple2.tuple;
    }
#

The way it works is template meta-programming. The type parameters in Entity<A, B, C>, Entity<C, A, B> and Entity<A, C, B> are actually sorted at compile time to produce a consistently ordered tuple. It also always places the largest elements at the beggining. That way, Entities with the same types actaully have the same structure, meaning you can freely cast between them

#

After compiling with optimization, access is a single-offset instruction. And benhmarked, it performs exactly like a normal tuple

unique shadow
#

I actually just finished implementing almost exactly the same thing.