Note that this is not an optimal solution (see the bottom of the post for problems), but a somewhat viable way of combining templates and virtual functions. I publish it in the hope that you can use it as a basis for creating something more effective. If you cannot find a way to improve this, I would suggest an Entity pattern as another answer .
If you don't want to make any major changes to Entity , you can implement a hidden virtual helper function in World to actually create the component. In this case, the helper function can take a parameter that indicates which component should be built, and return void* ; createComponent() calls a hidden function, specifying ComponentType , and returns the return value of ComponentType* . The easiest way I can imagine is to provide each component with a static member function, create() and map type indices for create() calls.
So that each component can take different parameters, we can use a helper type, let's call it Arguments . This type provides a simple interface, wrapping the actual list of parameters, allowing us to easily define our create() functions.
// Argument helper type. Converts arguments into a single, non-template type for passing. class Arguments { public: struct ArgTupleBase { }; template<typename... Ts> struct ArgTuple : public ArgTupleBase { std::tuple<Ts...> args; ArgTuple(Ts... ts) : args(std::make_tuple(ts...)) { } // ----- const std::tuple<Ts...>& get() const { return args; } }; // ----- template<typename... Ts> Arguments(Ts... ts) : args(new ArgTuple<Ts...>(ts...)), valid(sizeof...(ts) != 0) { } // ----- // Indicates whether it holds any valid arguments. explicit operator bool() const { return valid; } // ----- const std::unique_ptr<ArgTupleBase>& get() const { return args; } private: std::unique_ptr<ArgTupleBase> args; bool valid; };
Then we define our components to have a create() function that takes const Arguments& and takes arguments out of it by calling get() , dereferencing the pointer, dragging the pointer to ArgTuple<Ts...> to match the list of component constructor parameters and, finally getting the actual tuple of arguments using get() .
Please note that this will happen if Arguments was created with the wrong argument list (one that does not match the parameter constructor parameter list), just like calling the constructor directly with the wrong argument list; however, it accepts an empty argument list, however, due to Arguments::operator bool() , allowing us to provide default parameters. [Unfortunately, at the moment, this code has problems with type conversion, especially when the types do not have the same size. I'm still not sure how to fix this.]
// Two example components. class One { int i; bool b; public: One(int i, bool b) : i(i), b(b) {} static void* create(const Arguments& arg_holder) { // Insert parameter types here. auto& args = static_cast<Arguments::ArgTuple<int, bool>&>(*(arg_holder.get())).get(); if (arg_holder) { return new One(std::get<0>(args), std::get<1>(args)); } else { // Insert default parameters (if any) here. return new One(0, false); } } // Testing function. friend std::ostream& operator<<(std::ostream& os, const One& one) { return os << "One, with " << one.i << " and " << std::boolalpha << one.b << std::noboolalpha << ".\n"; } }; std::ostream& operator<<(std::ostream& os, const One& one); class Two { char c; double d; public: Two(char c, double d) : c(c), d(d) {} static void* create(const Arguments& arg_holder) { // Insert parameter types here. auto& args = static_cast<Arguments::ArgTuple<char, double>&>(*(arg_holder.get())).get(); if (arg_holder) { return new Two(std::get<0>(args), std::get<1>(args)); } else { // Insert default parameters (if any) here. return new Two('\0', 0.0); } } // Testing function. friend std::ostream& operator<<(std::ostream& os, const Two& two) { return os << "Two, with " << (two.c == '\0' ? "null" : std::string{ 1, two.c }) << " and " << two.d << ".\n"; } }; std::ostream& operator<<(std::ostream& os, const Two& two);
Then, with all this in place, we can finally implement Entity , World and WorldImpl .
// This is the world interface. class World { // Actual worker. virtual void* create_impl(const std::type_index& ctype, const Arguments& arg_holder) = 0; // Type-to-create() map. static std::unordered_map<std::type_index, std::function<void*(const Arguments&)>> creators; public: // Templated front-end. template<typename ComponentType> ComponentType* createComponent(const Arguments& arg_holder) { return static_cast<ComponentType*>(create_impl(typeid(ComponentType), arg_holder)); } // Populate type-to-create() map. static void populate_creators() { creators[typeid(One)] = &One::create; creators[typeid(Two)] = &Two::create; } }; std::unordered_map<std::type_index, std::function<void*(const Arguments&)>> World::creators; // Just putting in a dummy parameter for now, since this simple example doesn't actually use it. template<typename Allocator = std::allocator<World>> class WorldImpl : public World { void* create_impl(const std::type_index& ctype, const Arguments& arg_holder) override { return creators[ctype](arg_holder); } }; class Entity { World* world; public: template<typename ComponentType, typename... Args> void assign(Args... args) { ComponentType* component = world->createComponent<ComponentType>(Arguments(args...)); std::cout << *component; delete component; } Entity() : world(new WorldImpl<>()) { } ~Entity() { if (world) { delete world; } } }; int main() { World::populate_creators(); Entity e; e.assign<One>(); e.assign<Two>(); e.assign<One>(118, true); e.assign<Two>('?', 8.69); e.assign<One>('0', 8); // Fails; calls something like One(1075929415, true). e.assign<One>((int)'0', 8); // Succeeds. }
See in action here .
However, this has several problems:
- Uses
typeid for create_impl() , losing the benefits of subtracting compile time type. This results in slower execution than pattern.- Combining the problem,
type_info does not have a constexpr constructor, even if the typeid parameter is LiteralType .
- I'm not sure how to get the actual
ArgTuple<Ts...> type from Argument , and not just casting and prayer. Any ways to do this is likely to depend on RTTI, and I can't think of a way to use it to match type_index es or anything similar to the various template specializations.- In this regard, the arguments must be implicitly converted or entered on the
assign() call site instead of allowing the type system to do this automatically. This is ... a bit of a problem.