Is there a way to iterate over std::tuple and std::array using the same index?
Asked Answered
S

3

5

I am writing a simple Entity Component System framework in which I want to use variadic templates to get more flexible interface. For each component I have offset (from the begining of chunk's memory) stored in std::array. In my 'update()' method I would like to read offset from this array, add it to chunk's pointer and pass pointer (to specific component) directly to lambda as a parameter. I tried to use std::index_sequence, but I wasn't able to use this index as an index of tuple and array at the same time. Thanks in advance for help.

    template<typename ...Cn>
    class SystemGroup {
        public:
            using OffsetArray = std::array<uint16_t, sizeof...(Cn)>;

            static constexpr size_t kMaxGroups = 16;
            static constexpr GroupIndex kInvalidIndex = -1;

            struct Group {
                static constexpr uint8_t kNumComponents = sizeof...(Cn);

                OffsetArray componentOffsets;
                Chunk *pFirstChunk;
            };
    };

    template<typename ...Cn>
    void SystemGroup<Cn...>::update() {
        for (auto group : m_groups) {
            // iterate over archetype's chunks
            ecs::Chunk *pChunk = group.pFirstChunk;
            do {
                // get component data
                std::tuple<Cn*...> pointers;
                
                // Here is the problem. I don't know how to iterate over tuple and array using variadic templates
                // pointers[0] = pChunk->memory + m_groups.componentOffsets[0];
                // pointers[1] = pChunk->memory + m_groups.componentOffsets[1];
                // pointers[sizeof..(Cn)] = pChunk->memory + m_groups.componentOffsets[sizeof..(Cn)];

                auto updateComponents = [](int *pIntegers, float *pFloats) {
                };
                std::apply(updateComponents, pointers);
                pChunk = pChunk->header.pNext;
            } while(pChunk);
        }
    }

EDIT Thank you all for your help. I decided to choose solution proposed by max66. Of course I split definition and call of lambda to make it more readable.

Siana answered 16/3, 2021 at 19:8 Comment(3)
C++20 is OK? Or at least C++17?Erigena
Yes, c++20 is OKSiana
Can you please provide a minimal reproducible example? There are things that are used but not declared (what is m_groups?) and there are things that are declared but not used (all the constants). I'm also not sure what updateComponents is supposed to do (pointers is a tuple of Cn*... but updateComponents takes two pointers?). Is that relevant to the question? Or is the question just how to construct pointers?Ambrosius
C
2

Add a few helper functions:

template <typename Integer, Integer ...I, typename F>
constexpr void constexpr_for_each(std::integer_sequence<Integer, I...>, F &&func)
{
    (func(std::integral_constant<Integer, I>{}) , ...);
}

template <auto N, typename F>
constexpr void constexpr_for(F &&func)
{
    if constexpr (N > 0)
        constexpr_for_each(std::make_integer_sequence<decltype(N), N>{}, std::forward<F>(func));
}

Then you can do this:

constexpr_for<sizeof...(Cn)>([&](auto index)
{
    constexpr auto i = index.value;
    std::get<i>(pointers) = pChunk->memory + m_groups.componentOffsets[i];
});
Celebrity answered 16/3, 2021 at 19:17 Comment(0)
E
2

Given that you accept a C++20 answer... what about using a template lambda with a fold expression inside it ? [caution: code not tested]

std::tuple<Cn*...> pointers;

[&]<std::size_t ... Is>(std::index_sequence<Is...>)
   { ((std::get<Is>(pointers) = pChunk->memory + m_groups.componentOffsets[Is]), ...); }
   (std::make_index_sequence<sizeof...(Cn)>{});
        
Erigena answered 16/3, 2021 at 19:31 Comment(4)
Man, immediately invoked lambdas are not very readable.Hedgehog
@Hedgehog - No, not immediately very readable at all. But where is the fun, if is simple?Erigena
Could use digraphs <: :> <% %> instead of [ ] { } to really increase the unreadability quotient.Interlink
digraphs are scarier than macros IMHORetire
R
2

You can use std::apply with a lambda and a fold-expression.

std::tuple<Cn*...> pointers;

auto doTheStuff = [&](auto&... ptrs) {
    std::size_t index = 0;
    (ptrs = pChunk->memory + m_groups.componentOffset[index++], ...);
}

std::apply(doTheStuff, pointers);

The order of evaluation for the built-in comma operator guarantees that all value computations and side effects happen from left to right.

Redford answered 16/3, 2021 at 20:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.