Search code examples
c++c++17variadic-templatestemplate-meta-programminggeneric-lambda

why std::integral_constant is necessary in exploding a std::tuple?


#include <iostream>

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

template <std::size_t... Idx>
auto make_index_dispatcher(std::index_sequence<Idx...>) 
{
    return [](auto&& f) { (f(std::integral_constant<std::size_t, Idx>{}), ...); };
}

template <std::size_t N>
auto make_index_dispatcher() 
{
    return make_index_dispatcher(std::make_index_sequence<N>{});
}

template <typename Tuple, typename Func>
void for_each(Tuple&& t, Func&& f) 
{
    constexpr auto n = std::tuple_size<std::decay_t<Tuple>>::value;
    auto dispatcher = make_index_dispatcher<n>();
    dispatcher([&f, &t](auto idx) { f(std::get<idx>(std::forward<Tuple>(t))); });
}

int main() 
{
    for_each(std::make_tuple(1, 42.1, "hi"), [](auto&& e) {std::cout << e << ","; });
}

Question 1> Why I have to use std::integral_constant<std::size_t, Idx>{} instead of simply Idx in the following statement? Based on my understanding, std::integral_constant<std::size_t, Idx> is a type. Is it true that std::integral_constant<std::size_t, Idx>{} is a value of Idx?

// OK

template <std::size_t... Idx>
auto make_index_dispatcher(std::index_sequence<Idx...>) 
{
    return [](auto&& f) { (f(std::integral_constant<std::size_t, Idx>{}), ...); };
}

// Error

template <std::size_t... Idx>
auto make_index_dispatcher(std::index_sequence<Idx...>) 
{
    return [](auto&& f) { (f(Idx), ...); };
}

Is it true that std::get expected compile-time constant expression while Idx is NOT a compile-time constant expression?

Question 2> Why we cannot pass std::index_sequence by reference?

// Error: auto make_index_dispatcher(std::index_sequence<Idx...>&)

Thank you


Solution

  • Why I have to use std::integral_constant{} instead of simply Idx in the following statement?

    Because function arguments are never constant expressions. You can simply pass the indices as non-type template parameters instead, which are constant expressions. This works especially well with a C++20 template lambda:

    template <std::size_t... Idx>
    auto make_index_dispatcher(std::index_sequence<Idx...>) 
    {
        return [](auto&& f) { (f.template operator()<Idx>(), ...); };
    }
    
    template <typename Tuple, typename Func>
    void for_each(Tuple&& t, Func&& f) 
    {
        constexpr auto n = std::tuple_size<std::decay_t<Tuple>>::value;
        auto dispatcher = make_index_dispatcher<n>();
        dispatcher([&f, &t]<auto Idx>(){ f(std::get<Idx>(std::forward<Tuple>(t))); });
    }
    

    live example on godbolt.org


    Why we cannot pass std::index_sequence by reference?

    You can, but you need to invoke your function with an lvalue like with any other non-const reference. This compiles:

    template <std::size_t... Idx>
    auto make_index_dispatcher(std::index_sequence<Idx...>&) 
    {
    }
    
    int main()
    {
        std::index_sequence<> is;        
        make_index_dispatcher(is);
    }
    

    Also, it's completely useless.


    Also, your entire code can simply be:

    int main() 
    {
        std::apply([](auto&&... xs)
        {
            ((std::cout << xs << ','), ...);
        }, std::make_tuple(1, 42.1, "hi"));
    }