5 votes

Contourner l'absence de fonctions virtuelles modélisées en C++.

Je ne sais pas comment formuler ma question, mais je ne demande pas comment mettre en œuvre des fonctions virtuelles modélisées en soi. Je construis un système de composants d'entité, et j'ai deux classes importantes : - les fonctions virtuelles. World y Entity . World est en réalité une classe abstraite, et l'implémentation (appelons-la WorldImpl ) est une classe modèle qui permet l'utilisation d'un allocateur personnalisé (un qui peut être utilisé avec la fonction std::allocator_traits ).

Les composants sont tous les types de données que nous pouvons attacher aux entités. Cela se fait en appelant une fonction modèle nommée assign sur l'entité.

Voici le problème : j'essaie de faire en sorte que l'entité utilise l'allocateur du monde lors de la création et de l'initialisation des composants. Dans un monde parfait, vous appelleriez Entity::assign<ComponentType>( ... ) qui demanderait à la WorldImpl pour créer le composant avec l'allocateur approprié. Il y a cependant un problème ici - L'entité a un pointeur sur World et les fonctions virtuelles modélisées ne sont pas possibles à ma connaissance.

Voici une illustration un peu plus poussée qui pourrait rendre le problème plus évident :

class Entity
{
  template<typename ComponentType>
  void assign(/* ... */)
  {
    /* ... */
    ComponentType* component = world->createComponent<ComponentType>(/* ... */);
    /* ... */
  }

  World* world;
};

// This is the world interface.
class World
{
  // This is the ideal, which isn't possible as it would require templated virtual functions.
  template<typename ComponentType>
  virtual ComponentType* createComponent(/* ... */) = 0;
};

template<typename Allocator>
class WorldImpl : public World
{
  template<typename ComponentType> // again, not actually possible
  virtual ComponentType* createComponent(/* ... */)
  {
    // do something with Allocator and ComponentType here
  }
};

Étant donné que le code ci-dessus n'est pas réellement possible, voici la vraie question : Avec une telle hiérarchie de classes, quelle magie noire dois-je faire pour qu'une fonction soit appelée avec les paramètres des modèles ComponentType et Allocator ? C'est le but ultime - une fonction appelée sur un objet avec les deux paramètres de modèle à sa disposition.

2voto

Rumburak Points 1205

Je dirais que les Entités appartiennent à un certain type de monde et j'en ferais des modèles avec un paramètre Monde. Ensuite, on peut oublier tout l'héritage et le virtual et implémenter seulement les mondes qui remplissent l'interface requise, par ex.

template<typename World>
class Entity
{
  template<typename ComponentType>
  void assign(/* ... */)
  {
    /* ... */
    ComponentType* component = world.createComponent<ComponentType>(/* ... */);
    /* ... */
  }

  World world;
};

template<typename Allocator>
class WorldI
{
  template<typename ComponentType>
  ComponentType* createComponent(/* ... */)
  {
    // do something with Allocator and ComponentType here
  }
};

1voto

Justin Time Points 563

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X