Notez qu'il ne s'agit pas d'une solution optimale (voir le bas de l'article pour les problèmes), mais d'un moyen assez viable de combiner les modèles et les fonctions virtuelles. Je la publie dans l'espoir que vous puissiez l'utiliser comme base pour trouver une solution plus efficace. Si vous ne parvenez pas à trouver un moyen de l'améliorer, je vous suggère d'utiliser des modèles Entity
comme l'autre réponse suggéré.
Si vous ne voulez pas faire de modifications majeures à Entity
vous pouvez implémenter une fonction d'aide virtuelle cachée dans la fonction World
pour créer réellement le composant. Dans ce cas, la fonction d'aide peut prendre un paramètre qui indique le type de composant à construire, et renvoyer void*
; createComponent()
appelle la fonction cachée, en spécifiant ComponentType
et convertit la valeur de retour en ComponentType*
. Le moyen le plus simple auquel je pense est de donner à chaque composant une fonction membre statique, create()
et les indices de type de carte à create()
appels.
Pour permettre à chaque composant de prendre différents paramètres, nous pouvons utiliser un type d'aide, appelons-le Arguments
. Ce type fournit une interface simple tout en enveloppant la liste des paramètres réels, ce qui nous permet de définir facilement nos propres paramètres. create()
fonctions.
// 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;
};
Ensuite, nous définissons nos composants pour qu'ils aient un create()
qui prend un const Arguments&
et en retire des arguments, en appelant get()
le déréférencement du pointeur, l'intégration de l'objet pointé dans la base de données. ArgTuple<Ts...>
pour qu'il corresponde à la liste des paramètres du constructeur du composant, et enfin obtenir le tuple d'arguments réel avec get()
.
Notez que cela échouera si le Arguments
a été construit avec une liste d'arguments incorrecte (qui ne correspond pas à la liste de paramètres du constructeur du composant), tout comme le ferait l'appel direct du constructeur avec une liste d'arguments incorrecte ; il sera accepte une liste d'arguments vide, cependant, à cause de Arguments::operator bool()
permettant de fournir des paramètres par défaut. [Malheureusement, pour le moment, ce code a des problèmes avec la conversion de type, en particulier lorsque les types ne sont pas de la même taille. Je ne sais pas encore comment résoudre ce problème].
// 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);
Ensuite, avec tout cela en place, nous pouvons enfin mettre en œuvre Entity
, World
y 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.
}
Voyez-le en action aquí .
Cela dit, il y a quelques problèmes :
- S'appuie sur
typeid
pour create_impl()
en perdant les avantages de la déduction de type au moment de la compilation. Cela se traduit par une exécution plus lente que si elle était modélisée.
- Ce qui aggrave le problème,
type_info
n'a pas de constructeur constexpr, pas même lorsque l'option typeid
est un paramètre LiteralType
.
- Je ne suis pas sûr de savoir comment obtenir l'actuel
ArgTuple<Ts...>
type de Argument
plutôt que de simplement jeter et prier. Toute méthode permettant de le faire dépendrait probablement de RTTI, et je ne vois pas comment l'utiliser pour faire correspondre type_index
es ou tout autre élément similaire aux différentes spécialisations des modèles.
- De ce fait, les arguments doivent être implicitement convertis ou coulés au niveau de l'interface de l'utilisateur.
assign()
le site d'appel, au lieu de laisser le système de type le faire automatiquement. Ceci... est un peu un problème.