c++ – tuple foreach, index, find: return compile-time constant during runtime

Tuple foreach is relatively simple using either recursion or std::apply:

#include <cstring>
#include <iostream>
#include <tuple>
#include <utility>

template<typename F, typename T>
auto foreach_apply(F&& f, T &&t) {
    return std::apply([&f](auto&&... elements) {
        return (f(std::forward<decltype(elements)>(elements)) || ...);
    }, std::forward<T>
}

template <std::size_t I=0, typename F, typename... Ts>
void foreach_recurse(F&& f, std::tuple<Ts...> t) {
    if constexpr (I == sizeof...(Ts)) {
        return;
    } else {
        f(std::get<I>
        find<I+1>(f, t);
    }
}

int main() {
   //auto a = std::tuple(true, true, false, false, true);
   auto a = std::tuple("one", "two", "three", "four", "five");
   //auto b = foreach_recurse([](auto& element) -> bool {
   auto b = foreach_apply([](auto& element) -> bool {
       std::cout << element << std::endl;
       if (!strcmp("three", element))
           return true;
       else
           return false;
   }, a);
   std::cout << "Done" << std::endl;
   std::cout << b << std::endl << std::endl;
}

Indexing is only slightly trickier:

template <std::size_t I=0, typename F, typename... Ts>
size_t index_recurse(F&& f, std::tuple<Ts...> t) {
    if constexpr (I == sizeof...(Ts)) {
        return -1;
    } else {
        auto e = std::get<I>
        if (f(e))
            return I;
        return index_recurse<I+1>(f, t);
    }
}

template <std::size_t I=0, typename F, typename... Ts>
bool index_recurse_2(F&& f, std::tuple<Ts...> t, size_t* i) {
    if constexpr (I == sizeof...(Ts)) {
        return false;
    } else {
        auto e = std::get<I>
        if (f(e)) {
            *i = I;
            return true;
        }
        return index_recurse_2<I+1>(f, t, i);
    }
}

template<typename F, typename T>
auto index_apply(F&& f, T &&t, size_t* ix) {
    return std::apply([&f,ix] (auto&&... elements) {

        return [&f,ix]<std::size_t... I>(std::index_sequence<I...>, auto&&... elements) {
             
             auto fi = [&f,ix](auto i, auto&& element) {
                 auto r = f(std::forward<decltype(element)>(element));
                 if (r)
                     *ix = i;
                 return r;
             };
             return (fi(I, std::forward<decltype(elements)>(elements)) || ...);

        }
         ( std::make_index_sequence<sizeof...(elements)>()
         , std::forward<decltype(elements)>(elements)...
         );

    }, std::forward<T>
}

int main() {
   /*
   auto a = std::tuple("one", "two", "three", "four", "five");
   auto b = index_recurse([](auto& element) -> bool {
       std::cout << element << std::endl;
       if (!strcmp("three", element))
           return true;
       else
           return false;
   }, a);
   std::cout << "Done" << std::endl;
   std::cout << b << std::endl << std::endl;
   */

   /*
   auto a = std::tuple("one", "two", "three", "four", "five");
   size_t b;
   auto c = index_recurse_2([](auto& element) -> bool {
       std::cout << element << std::endl;
       if (!strcmp("three", element))
           return true;
       else
           return false;
   }, a, &b);
   std::cout << "Done" << std::endl;
   std::cout << b << std::endl << std::endl;
   */

   /*
   auto a = std::tuple("one", "two", "three", "four", "five");
   size_t b;
   auto c = index_apply([](auto& element) -> bool {
       std::cout << element << std::endl;
       if (!strcmp("three", element))
           return true;
       else
           return false;
   }, a, &b);
   std::cout << "Done" << std::endl;
   std::cout << b << std::endl << std::endl;
   */
}

Now, get the value rather than the index. The index_* functions show that this is possible. We take a compile-time value (tuple) and a set of compile-time functions (unrolled index functions), apply a runtime function that matches an input, and get a runtime value that depends on the compile-time values. This is what the find_* functions attempt to do.

Given a tuple T of length N, find_broken unrolls to N functions of type (i:0-N, T) where each function either returns the i-th function, or returns from the next in the sequence. That means the return type of the i-th function must match all the previous return types. So this recursive approach can’t work.

template <std::size_t I=0, typename F, typename... Ts>
auto* find_broken_1(F&& f, std::tuple<Ts...> t) {
    if constexpr (I == sizeof...(Ts)) {
        // What type? Can this be fixed?
        return (const char**)&"<>";
        //return (void*)nullptr;
        //return ((decltype(std::get<I-1>)::type)*)nullptr;
    } else {
        auto& e = std::get<I>
        if (f(e))
            return &e;
        std::cout << e << std::endl;
        return find_broken_1<I+1>(f, t);
    }
}

Index is not known at compile-time so std::get doesn’t compile. It would be nice if c++ would template for every possible index so this would work during runtime, just like c++ already unrolls every possible index function.

template <typename F, typename T>
auto find_broken_2(F&& f, T&& t, bool* b) {
    const size_t i = index_recurse(f, t);
    if (i >= 0) {
        *b = true;
        return std::get<i>
    }
    else {
        *b = false;
        //return nullptr;
    }
}

We can generate a runtime value from a compile-time value. If the transform is reversible and the bounds are known, we should be able to lookup a compile-time value from a runtime value. How can we trick the compiler into doing so? If possible, integrate this into the index_* functions to avoid double iteration.

Moving something into a type forces it to become a compile-time something. Unfortunately this unrolls to a different return type for each generated function, and since the returns are chained, generates a compile error.

I’m just trying a bunch of different ideas, none working:

template<bool value, typename ...>
struct bool_type : std::integral_constant<bool , value> {};

//std::integral_constant<int, I> run_to_compile(size_t i, std::tuple<Ts...> t) {
template <std::size_t I=0, typename... Ts>
auto run_to_compile(size_t i, std::tuple<Ts...> t) {
    if constexpr (I == sizeof...(Ts)) {
        return std::integral_constant<int, I-1>();
        // replace with static_assert... nope
        //static_assert(bool_type<false, Ts...>::value, "wrong");
        //return I-1;
    }
    else {
        //if (i == I) {
        if (I > 2) {
            //return;
            return std::integral_constant<int, I>();
            //return I;
        }
        return run_to_compile<I+1>(i, t);
    }
}

template<int value, typename ...>
struct int_type : std::integral_constant<int , value> {};

template <int I=0, typename T>
constexpr auto run_to_compile_2(int i) {
    return int_type<i, T>();
}

template <typename F, typename T>
auto find_broken_3(F&& f, T&& t, bool* b) {
    const size_t ir = index_recurse(f, t);
    // nope
    //constexpr decltype
    // nope, again (really?)
    //auto ic = std::integral_constant<int, ir>();
    //constexpr auto ic = run_to_compile(0, temp);
    //const auto ic = run_to_compile(ir, t);
    //const auto ic = r2c_2<const int>(ir);
    run_to_compile_2<2, int>(2);
    const auto ic = 2;
    if (ir >= 0) {
        *b = true;
        return std::get<ic>
    }
    else {
        *b = false;
        //return nullptr;
    }
}

How can I fix this?

Leave a Comment