c++classtemplatesentitiesentity-component-system

Template classes and calling different constructors depending on iteration


I am learning ECS and I am now trying to implement components for my project.

So to put you in context, I have an aquarium, and many components in it (let's say seaweeds and fish). Both have an age, but only fishes have a race.

I have a class for general_components (age and other stuff) and a class for fish-specific components (race, sex, etc).

I made a template components class with a create method that looks like that:

template<typename ConcreteComponent> // ConcreteComponent is attached to the class
ConcreteComponent& Components<ConcreteComponent>::create( entity e ) {
    m_components.push_back( ConcreteComponent(e) );
    return m_components.back();
}

The problem I have is I want to be able to call different constructors depending on what class I have (instead of entity e right here, which is generic to every class in my setup), but while staying the most efficient way (so with templates and no copy paste for every class). For my problem it wouldn't be the end of the world but in general if I faced this again I want to be prepared.

Is there a way to call a create function with different parameters?

For example:

A<fishComponents> myClassIter;
myClassIter.create("name", age, race, sex)

For fishes would pass "name",age,race,sex to ConcreteComponent() constructor instead of only e (And I have a "name",age,race,sex constructor for fishComponents).

TL;DR: in a template class method, is it possible to pass a different number and nature of values to the constructor of the class used depending on the parameters

template<typename A>
void myClass<A>::create( list_of_parameters) {
A(list_of_parameters) /*calls the constructor of the template class A */
}

I saw something like that in C but it was recommended not to touch it as it was dated and not used any more.


Solution

  • The solution here is to use parameter packs and std::forward to pass along whatever arguments come in to the real constructor. A complete but simplified example (no data structures and one argument per type) is below:

    #include <iostream>
    
    class Fish {
    public:
        Fish(std::string const &name) {
            std::cout << "Making a fish named " << name << '\n';
        }
    };
    
    class Seaweed {
    public:
        Seaweed(int length) {
            std::cout << "Making a seaweed that's " << length << " feet long\n";
        }
    };
    
    template <typename ConcreteComponent, typename ...ARGS>
    ConcreteComponent create(ARGS && ...args) {
        return ConcreteComponent(std::forward<ARGS>(args)...);
    }
    
    int main() {
        create<Fish>("Bob");
        create<Seaweed>(42);
        return 0;
    }
    

    Output:

    $ ./forwarding
    Making a fish named Bob
    Making a seaweed that's 42 feet long
    

    It's worth reading about std::forward, but what we're basically doing here is taking everything that comes in to create and passing it along to the type's constructor, while also preserving properties about the type (e.g., whether it's a temporary or not). Thus, you can pass anything, so long as there's a valid constructor to pass things on to.

    My code was tested with g++-7.3.0 using C++11, 14, and 17.