Marcus Aseth
Marcus Aseth

Reputation: 129

Best way to enumerate the types on a tuple?

As the title says, I'm looking for the best way to enumerate the types on a tuple. So my first attempt was the code below, which is an adaption of another answer here on stackoverflow, with the only change being that is wrapped on another class, so this doesn't require to pass the tuple type as template parameter:

#include <tuple>
using namespace std;
using uInt = size_t;

template<typename... ARGS>
class ECS_Data
{
private:
    tuple<ARGS...> data;

private:
    //TYPE INDEXING
    template<typename T, typename Types>
    struct Internal_Type_Index;

    template<typename T>
    struct Type_Index {
        static constexpr uInt value = Internal_Type_Index<T, tuple<ARGS...>>::value;
    };

    template<typename T, typename U, typename... Types>
    struct Internal_Type_Index<T, tuple<U, Types...>> {
        static constexpr uInt value = 1 + Internal_Type_Index<T, tuple<Types...>>::value;
    };

    template<typename T, typename... Types>
    struct Internal_Type_Index<T, tuple<T, Types...>> {
        static constexpr uInt value = 0;
    };

public:
    template<typename T>
    static constexpr uInt Type_Index_v = Type_Index<T>::value;

};

int main()
{
  ECS_Data<int,float,double,long long,char> ecs_data;
  return ecs_data.Type_Index_v<char>; //return 4
}

The "problem" with this is that for a tuple with 50 types let's say, it instantiate 1350 structs and each come with a static variable. For example, for tuple<int,float,double>: called with int would instantiate 1 struct Internal_Type_Index<int, tuple<int, Types...>> and stop because int is immediately found. Called with float would instantiate 2 structs Internal_Type_Index<float, tuple<int, Types...>> and then Internal_Type_Index<float, tuple<float, Types...>> then stop because float is found, and so on...resulting in 3+2+1 instantiations (assuming was Type_Index_v was called for all types). So N + (N+N^2)/2 instantiation, right? N Type_Index and (N+N^2)/2 Internal_Type_Index partial specializations (all 1350 with a static variable inside of them).

My second solution is this:

#include <tuple>
#include <utility>
using namespace std;
using uInt = size_t;

template<typename... ARGS>
class ECS_Data
{
private:
    tuple<ARGS...> data;

    template<typename T>
    inline static uInt _index = 0;

public:
    ECS_Data() {
        init_index(std::make_index_sequence<sizeof...(ARGS)>());
    }

    template<typename T>
    uInt index()const { return _index<T>; }

private:
    template<size_t... N>
    void init_index(std::index_sequence<N...>) { (..., (_index<ARGS> = N)); }

};

int main()
{
  ECS_Data<int,float,double,char> ecs_data;
  return ecs_data.index<char>();
}

Still with the example of a tuple of 50 types, this time we have: 2+N instantiations from make_index_sequence 50 static templated variables from _index 50 member function instantiations from index<T>() This seems better number wise, but it is runtime now, because I don't see a way to make _index constexpr while initializing it with a fold expression like in the code above.

So my questions would be: 1)Do you see a way to improve the code above? 2)Considering all (for example compiler optimization magic), at the end of the day which version is better? 3)There is a yet better version you can suggest? EDIT:Forgive if is hard to read, the text editor here on stackoverflow completely ignored my newlines.

Upvotes: 1

Views: 113

Answers (2)

Jarod42
Jarod42

Reputation: 217860

I would do:

template <typename T, typename Tuple, std::size_t ... Is>
constexpr std::size_t tuple_index_impl(std::index_sequence<Is...>)
{
    // You might adapt here to handle duplicate differently
    return std::max({ std::is_same<T, std::tuple_element_t<Is, Tuple>>::value * (Is + 1)... });
}

template <typename T, typename Tuple>
constexpr std::size_t tuple_index()
{
   return tuple_index_impl<T, Tuple>(std::make_index_sequence<std::tuple_size<Tuple>::value>());
}

Demo

One instantiation of both methods by type + std::index_sequence (which might be done magically by compiler in 1 unique instantiation).

Upvotes: 2

N. Shead
N. Shead

Reputation: 3938

I'll only take a shot at 3). My shot would look something like this:

template <typename Tuple, typename Needle, std::size_t... I>
constexpr std::size_t tuple_index_impl(std::index_sequence<I...>) {
    // don't use std::disjunction_v here as it will return bool
    constexpr std::size_t idx = std::disjunction<
        // need I+1 to prevent a falsy result on index 0
        std::integral_constant<std::size_t, 
            (I+1) * std::is_same_v<std::tuple_element_t<I, Tuple>, Needle>>...,
        // fallthrough case
        std::integral_constant<std::size_t, 0>
    >::value;
    static_assert(idx != 0, "type not found in tuple");
    return idx - 1;
}

template <typename Tuple, typename Needle>
constexpr std::size_t tuple_index() {
    return tuple_index_impl<Tuple, Needle>(
        std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

Which could be used like

using T = std::tuple<int, long long, double, char, double>;
//std::cout << tuple_index<T, float>();   //ERROR: type not found in tuple
std::cout << tuple_index<T, double>();  // prints "2", the first one
std::cout << tuple_index<T, char>();  // prints "3"

Live: http://coliru.stacked-crooked.com/a/8ea39202298f2ddc

The key here is std::disjunction, which doesn't need to instantiate any template arguments after the value it has found. This will also work on any tuple-like type (things that implement the tuple_element and tuple_size interface).

Since this is obviously all constexpr/template magic, the only costs paid will be at compile time. How well this performs in that regard will depend on the efficiency of the compiler's std::tuple_element implementation, I would guess, but I haven't done any profiling.

Upvotes: 1

Related Questions