Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use and access a template parameter pack of parameter packs

Short Intro

I am trying to create a AddComponents() method that creates and adds multiple components to an entity at once. I have already written a working method to add one component at a time.

It has the following signature:

template <class TComponent, typename... TArguments>
TComponent & AddComponent(TArguments&&... arguments);

and is used in the following way

entity.AddComponent<SomeComponent>(data1, data2, data3);

I would now like to make a function that adds multiple components at one i.e. takes a parameter pack of TComponents. Of course, the data must be passed as well and here is where things get ugly; I basically need a parameter pack of parameter packs. The function should then iterate over the TComponents (e.g. using int i < sizeof...(Types)) and call AddComponent<T>(data...) for each TComponent. I have worked with templates quite a bit but I struggle to get my head around this.

What I want

Ideally, I want it to be used something like this:

entity.AddComponents<PositionComponent, SpriteComponent, TComponent>(
{ position },
{ texture, colour },
{ data1, data2 });

Internally I need a way to do something like

for each TComponent : TComponents
{
    this->AddComponent<TComponent>(forward...(data))
}

In theory, I might be able to get away without this but it seems like an interesting problem nevertheless.

AddComponent Code

In case people might question what the function does here's the code.

template <class TComponent, typename... TArguments>
inline TComponent & Entity::AddComponent(TArguments&&... arguments)
{
    auto typeId = detail::GetComponentTypeID<TComponent>();

    auto component = std::make_shared<TComponent>(eventManager, *this, std::forward<TArguments>(arguments)...);

    componentList.push_back(component);
    componentDictionary.insert(std::make_pair(typeId, component));

    entityManager.AddEntityToGroup(*this, typeId);

    this->AddPolymorphism(typeId, component);
    eventManager.RaiseEvent(mbe::event::ComponentsChangedEvent(*this));

    return *component;
}
like image 650
Adrian Albert Koch Avatar asked Mar 04 '23 05:03

Adrian Albert Koch


2 Answers

I would use something similar to what the STL ended up doing: Pass packs of packs as a pack of tuple.

Take emplace_back in a pair for example:

stuct Widget {
    int a;
    std::string name = {};
};

std::pair<Widget, Widget>{
    std::piecewise_construct,
    std::forward_as_tuple(12, "values"),
    std::forward_as_tuple(31)
};

Here the first parameter tell that you want piecewise construct, so each argument will act as a "pack" that will be expanded into the members of the pair. In this case, the elements of the pair was construct as if:

Widget first{12, "values"};
Widget second{31};

In your case, it seems you always want to construct N components. So the solution would be:

template<typename Component, typename Tuple>
void addComponentUnpack(Tuple&& tuple) {
    std::apply([](auto&&... args) {
        addComponent<Component>(std::forward<decltype(args)>(args)...);
    }, std::forward<Tuple>(tuple));
}

template<typename... Components, typename... Tuples>
void addComponents(Tuples&&... tuples) {
    (addComponentUnpack<Components, Tuples>(std::forward<Tuples>(tuples)), ...);
}

So for each Component, you have a corresponding Tuple in the packs Components and Tuples. Then, each tuples are sent to a function that construct a single component of type Component using the tuple Tuple.

This example uses C++17 features such as fold expressions and std::apply but it can be implemented in C++14 with a bit more code.


If however you don't wish to use tuples as parameters, you could alway recieve instances of your types directly:

template<typename... Components>
void addComponents(Components&&... components) {
    // ...
}

It's usage would be like:

addComponents(
    PositionComponent{12, 43},
    CollisionComponent{},
    SpriteComponent{"texture name"}
);

// or

addComponents<PositionComponent, CollisionComponent, SpriteComponent>(
    {12, 42},
    {},
    {"texture name"}
);

Of course, that would require to move component from the parameters into the entity, which may not be entirely free, depending on the case.

like image 118
Guillaume Racicot Avatar answered Mar 23 '23 16:03

Guillaume Racicot


template<class...Ts>
struct types_t:
  std::integral_constant<std::size_t, sizeof...(Ts)>
{};
template<class T0, class...Ts>
struct types_t<T0, Ts...>:
  std::integral_constant<std::size_t, 1+sizeof...(Ts)>

{ using head=T0; using tail=types_t; };

template<class Componets>
auto ComponentWorker( Components ) {
  return [this](auto components, auto&&...args) {
    using TComp0 = typename decltype(components)::head;
    // add TComp0 using all of args...
    using TCompTail = typename decltype(components)::tail;
    if constexpr( TCompTail::value != 0 )
      return ComponentWorker( TCompTail{} );
    else
      return; // void
  }
}
template <class TC0, class... TCs, class... TArguments>
auto AddComponent(TArguments&&... arguments) {
  using components = types_t<TC0, TCs...>;
  return ComponentWorker( components )( std::forward<TArguments>(arguments)... );
}

the syntax here is:

AddComponent<A,B,C>( A_args... )( B_args... )( C_args... );

failing to pass arguments results in the component not being added.

like image 36
Yakk - Adam Nevraumont Avatar answered Mar 23 '23 16:03

Yakk - Adam Nevraumont