Addressing the ith member of a std::tuple when i is not a constant expression?

Viewed 183

Suppose I have the following function template:

template<typename T>
void mutate(T& t) { /*...*/ }

and I have the following class template:

template<typename... Args>
class Processor {
    std::tuple<Args...> tup;

    // call mutate on the ith tuple element
    void process(size_t i) {
        mutate(std::get<i>(tup)); // ERROR
    }
}

This doesn't work because the i parameter of process isn't a constant expression. (It can't be a constant expression, because i isn't known until runtime)

Without modifying the signature of process or mutuate, is there some way to efficiently make this work? ie How can I change the implementation of process so that it calls mutate on the ith member of tup?

3 Answers

You need to take the runtime index and lift it into a constant expression. The easiest way to do that is to just use Boost.Mp11 which comes with a function just for this:

template<typename... Args>
class Processor {
    std::tuple<Args...> tup;

    // call mutate on the ith tuple element
    void process(size_t i) {
        mp_with_index<sizeof...(Args)>(i, [&](auto I){
            mutate(std::get<I>(tup));
        });
    }
}

What mp_with_index does is take a constant-expression for the max size (sizeof...(Args)) and the runtime size (i) and then invoke your callable (the lambda) with an integral constant that is the runtime size lifted up to a constant-expression.


This can be implemented yourself using std::index_sequence by building an array of function pointers and then invoking the correct one:

template <size_t... Is, typename F>
decltype(auto) mp_with_index(size_t i, F f, std::index_sequence<Is...>) {
    using R = decltype(f(std::integral_constant<size_t, 0>{}));
    using P = R(*)(F&);
    static constexpr P fns[] = {
        +[](F& f) -> R { return f(std::integral_constant<size_t, Is>{}); }...
    };
    return fns[i](f);
}

template <size_t N, typename F>
decltype(auto) mp_with_index(size_t i, F f) {
    return mp_with_index(i, f, std::make_index_sequence<N>());
}

(note that the Boost.Mp11 implementation is better than this, this is just functionally correct).

With some helper functions we can do it.

#include <tuple>
#include <iostream>

template<typename T>
void mutate(T& t)
{
    std::cout << typeid(t).name() << "\n";
}

template<typename T, std::size_t... Seq>
void processes_each(int i, T& t, std::index_sequence<Seq...> const&)
{
    bool discard[] = {  false,
                        (i == Seq ? (mutate(std::get<Seq>(t)), true) : false) ...
                     };
    (void)discard;
}

template<typename... Args>
class Processes
{
    std::tuple<Args...> tup;

    public:
    void process(size_t i) {
        processes_each(i, tup, std::make_index_sequence<sizeof...(Args)>());
    }
};

int main()
{
    Processes<int, double>      p1;
    p1.process(0);
}

Just for fun...

Waiting for C++20 (template lambda)

void process (std::size_t i)
 {
   [&]<std::size_t ... Is>(std::index_sequence<Is...> const &)
    { ((i == Is ? (mutate(std::get<Is>(tup)), 0) : 0), ...); }
     (std::index_sequence_for<Args...>{});
 }

in C++17 we can use a recursive generic lambda with std::integral_constant

   template <std::size_t I>
   using IC = std::integral_constant<std::size_t, I>;

   void process (std::size_t i)
    {
      auto l = [&](auto self, auto ic)
       { if constexpr ( ic < sizeof...(Args) )
            i == ic ? (mutate(std::get<ic>(tup)), 0)
                    : (self(self, IC<ic+1u>{}), 0); };

      l(l, IC<0u>{});
    }

The following is a full compiling example

#include <tuple>
#include <iostream>

template<typename T>
void mutate (T & t)
 { std::cout << t << "\n"; }

template<typename... Args>
struct Processes
 {
   std::tuple<Args...> tup;

   template <std::size_t I>
   using IC = std::integral_constant<std::size_t, I>;

   void process (std::size_t i)
    {
      auto l = [&](auto self, auto ic)
       { if constexpr ( ic < sizeof...(Args) )
            i == ic ? (mutate(std::get<ic>(tup)), 0)
                    : (self(self, IC<ic+1u>{}), 0); };

      l(l, IC<0u>{});
    }
};

int main()
 {
   Processes<char, int, long, long long>  p{{'0', 10, 200l, 3000ll}};

   p.process(2u);
 }
Related