4
votes

Les fonctions membres virtuelles basées sur des modèles sont interdites, existe-t-il des alternatives?

Disons que j'ai quelque chose comme ça:

class OuterBase
{
public:
    virtual void send(A a) = 0;
    virtual void send(B b) = 0;
    virtual void send(C c) = 0;
    virtual void send(D d) = 0;
};

class OuterDerived : public OuterBase
{
public:
    virtual void send(A a) override { ... }
    virtual void send(B b) override { innerBase->doSend(b); }
    virtual void send(C c) override { innerBase->doSend(c); }
    virtual void send(D d) override { innerBase->doSend(d); }
private:
    InnerBase* innerBase;
};

InnerBase étant une classe de base polymorphe:

class OuterBase
{
public:
    template <typename MessageType>
    virtual void send(MessageType message) = 0;
};

class OuterDerived : public OuterBase
{
public:
    void send(A a) { ... }
    template <typename MessageType>
    virtual void send(MessageType message) override { innerBase->send(message); }
private:
    InnerBase* innerBase;
};

Jusqu'ici tout va bien, et si je veux ajouter des surcharges, je dois juste les ajouter à InnerBase et InnerDerived .

À un moment donné, il s'avère que j'ai besoin de rendre la classe externe personnalisable aussi, donc j'aimerais pouvoir écrire quelque chose comme ceci:

class InnerBase
{
public:
    virtual void doSend(B b) = 0;
    virtual void doSend(C c) = 0;
    virtual void doSend(D d) = 0;
};

class InnerDerived : public InnerBase
{
public:
    virtual void doSend(B b) override { ... }
    virtual void doSend(C c) override { ... }
    virtual void doSend(D d) override { ... }
};

Mais ce n'est pas autorisé par la norme, qui interdit les fonctions membres virtuelles basées sur des modèles. Au lieu de cela, je suis obligé d'écrire toutes les surcharges une par une comme ceci:

class Outer
{
public:
    void send(A a) { ... }
    template <typename MessageType>
    void send(MessageType message) { innerBase->doSend(message); }
private:
    InnerBase* innerBase;
};

C'est beaucoup de code standard, et cela signifie que si jamais je veux ajouter une autre surcharge, je dois modifier 4 classes au lieu de seulement 2 (et deux fois plus de fichiers). En supposant que j'ai besoin de plus d'un niveau d'indirection, cela pourrait facilement aller jusqu'à 6, 8, etc., ce qui rend l'ajout de nouvelles surcharges totalement impraticable.

Est-ce que je fais ça mal? Y a-t-il un moyen plus propre de faire cela?

La seule alternative à laquelle je puisse penser serait de faire fuir le pointeur innerBase vers la classe de base afin qu'elle puisse appeler doSend() directement, ce qui permettrait de conserver le modèle send() , mais cela interrompt l'encapsulation et empêche OuterDerived d'ajouter le sien send() surcharges ainsi que d'ajouter sa propre logique autour des appels à doSend() ...


5 commentaires

Cela ressemble à quelque chose qui est normalement résolu avec le modèle de visiteur .


Vous pouvez également créer un modèle complet, jetez un œil à la structure des paquets de Boost Beast . Vous pourriez peut-être utiliser quelque chose de similaire et modéliser entièrement vos messages afin de ne pas avoir besoin de A , B , etc.


@FantasticMrFox À quoi cela ressemblerait-il ici? Faire en sorte que tous mes messages héritent d'un Message de type de base, en passant un Message * de la base extérieure à la base interne, puis en appelant message-> send (innerBase), qui serait polymorphe et appellerait la surcharge correcte avec innerBase-> send (this) ? j'ai l'impression qu'il y a un risque que j'aie besoin de surcharger message :: send () à un moment donné, de vouloir le modéliser et de me retrouver avec le même problème ...


"on a l'impression qu'il y a un risque" YAGNI . Vous résolvez un problème lorsque vous avez un problème. Supposons que vous ayez deux classes externes Green et Blue , et deux classes de message Soft et Hard , avez-vous vraiment besoin de quatre implémentations sendMessage non sendMessage , non réductibles à une sorte de matrice 2x2?


@ n.'pronons'm. Eh bien, pour commencer, je ne peux pas appeler message-> send (externalDerived) sans surcharge, c'est-à-dire que je ne peux pas gérer le cas send (A) de mon exemple. Dans mon problème actuel, les classes externes sont des protocoles de communication totalement indépendants, qui devraient prendre en charge l'envoi de divers types de données, et l'un des protocoles doit négocier la version de l'une de ses sous-sections à utiliser (les classes internes). Si j'emballe les données que j'envoie dans des classes héritant de Message, cela n'a aucun sens de faire dépendre Message de l'interface de la sous-section.


3 Réponses :


0
votes

Si vous souhaitez avoir un nombre fixe de surcharges à l' send , je vous suggère d'ajouter chaque surcharge à la vtable comme vous le faites déjà. Ensuite, pour chaque implémentation, il vous suffit de transmettre chaque définition de vtable à une définition de modèle définie dans chaque classe dérivée.

Si vous voulez un nombre illimité de surcharges, malheureusement, vous n'avez pas de chance ici. Pour ce faire en toute sécurité, cela nécessite une fonctionnalité de langage appelée types de rang supérieur ou de rang n et les types de rang supérieur nécessitent un certain niveau d'effacement de type comme les génériques de Java. Donc, à la place, vous devez faire un effacement de type manuel, où vous passez un pointeur et un dictionnaire, et faites attention à ne pas mélanger vos types.

Par exemple, considérez ce pseudo C ++:

// stuff you can do with A
struct Dictonary {
    std::function<void*()> create;
    std::function<void*(void*)> increment;
};

class OuterBase
{
public:
     virtual void send(void* a, Dirtonary) = 0;
};

et après effacement de type manuel:

// stuff you can do with A
erasure template <pointer A>
struct Dictonary {
    std::function<A()> create;
    std::function<A(A)> increment;
};

class OuterBase
{
public:
     erasure template <pointer A> virtual void send(A a, Dirtonary<A>) = 0;
};

De plus, si toutes les fonctions que vous devez utiliser pour A impliquent this vous pouvez définir une nouvelle classe de base plutôt que de vous fier au passage du dictionnaire.


0 commentaires

1
votes

C'est beaucoup de code standard, et cela signifie que si jamais je veux ajouter une autre surcharge, je dois modifier 4 classes au lieu de seulement 2 (et deux fois plus de fichiers).

Bien que plusieurs niveaux d'héritage sentent mauvais, la solution naïve pourrait être la meilleure ici: ajoutez une classe dérivée qui est InnerBase à InnerBase et en dérive:

using OuterBaseT = OuterBase<A, B, C, D>;
class OuterDerived : public OuterInnerProxy<OuterBaseT> {
public:
    OuterDerived(InnerBase* inner) : OuterInnerProxyBase(inner) { }
    using OuterBaseT::send; 
    virtual void send(A a) override { /* ... */ }
};

Maintenant, vous ne modifiez que 3 classes. Mais supposons que vous vouliez éviter de taper toutes ces surcharges du tout. Ensuite, vous pouvez obtenir toute la sur-ingénierie avec des modèles variadiques. Notre objectif sera de faire fonctionner ce travail:

template <typename TOuterBase, typename TOverloadTuple>
class OuterInnerProxyImpl;

template <typename... TOuterArgs, typename... TOverloads>
class OuterInnerProxyImpl<OuterBase<TOuterArgs...>, std::tuple<TOverloads...>> : public OuterInnerProxySingle<TOverloads, TOuterArgs...>... { };

template <typename T>
using OuterInnerProxy = OuterInnerProxyImpl<T, typename InnerBaseOverloads<T>::Type>;

Tout d'abord, la base externe variadique est assez simple si vous utilisez C ++ 17. Si ce n'est pas le cas, vous pouvez toujours le faire en C ++ 11 avec l'héritage récursif, qui est moins joli et plus lent à compiler:

class OuterInnerProxyBase {
public:
    OuterInnerProxyBase(InnerBase* innerBase) : innerBase(innerBase) { }
    InnerBase* innerBase;
};

template <typename T, typename... Ts>
class OuterInnerProxySingle : public virtual OuterInnerProxyBase, public virtual OuterBase<Ts...> {
public:
    using OuterBase<Ts...>::send;
    OuterInnerProxySingle() : OuterInnerProxyBase(nullptr) { }
    void send(T t) override { OuterInnerProxyBase::innerBase->doSend(t); }
};

La première partie de la création d' OuterInnerProxyT consiste à déterminer quels Ts dans OuterBase<Ts...> ont des surcharges dans InnerBase . Un moyen simple de le faire est d'utiliser SFINAE pour convertir chaque type en un tuple plein ou vide, puis de les écraser avec std::tuple_cat :

template <typename T, typename = void>
struct InnerBaseOverload {
    using Type = std::tuple<>;
};
template <typename T>
struct InnerBaseOverload<T, decltype(std::declval<InnerBase>().doSend(std::declval<T>()))> {
    using Type = std::tuple<T>;
 };
template <typename T>
struct InnerBaseOverloads;
template <typename...Ts>
struct InnerBaseOverloads<OuterBase<Ts...>> {
    using Type = decltype(std::tuple_cat(std::declval<typename InnerBaseOverload<Ts>::Type>()...));
};

Ensuite, définissez les classes qui remplacent send pour un seul type. Nous pouvons utiliser l'héritage virtuel pour nous assurer qu'ils ont une OuterBase et une InnerBase* . MSVC me crie dessus si je n'invoque pas explicitement la base virtuelle à tous les niveaux, mais elle ne sera pas appelée:

template <typename T>
class OuterBaseSingle
{
public:
    virtual void send(T t) = 0;
};

template <typename... Ts>
class OuterBase : OuterBaseSingle<Ts>...
{
public:
    using OuterBaseSingle<Ts>::send...;
};

Enfin, nous pouvons les combiner en utilisant une petite spécialisation partielle:

using OuterBaseT = OuterBase<A, B, C, D>;
using OuterInnerProxyT = OuterInnerProxy<OuterBaseT>; // overrides B, C, D
struct OuterDerived : OuterInnerProxyT {
    OuterDerived(InnerBase* innerBase);
    virtual void send(A a) override { /* ... */ }
};

Ajoutez un passe-partout supplémentaire pour initialiser la base virtuelle et lever les surcharges d' send , et c'est tout:

class OuterInnerProxy : public OuterBase
{
public:
    OuterInnerProxy(InnerBase* innerBase) : innerBase(innerBase) {}
    virtual void send(B b) override { innerBase->doSend(b); }
    virtual void send(C c) override { innerBase->doSend(c); }
    virtual void send(D d) override { innerBase->doSend(d); }
private:
    InnerBase* innerBase;
};

class OuterDerived : public OuterInnerProxy
{
public:
    OuterDerived(InnerBase* innerBase) : innerBase(innerBase) {}
    virtual void send(A a) override { /* ... */ }
}

Démo: https://godbolt.org/z/7z7Exo

Une mise en garde cependant: cela fonctionne correctement sur msvc 2019, clang 11 et gcc 10.1. Mais pour une raison quelconque, il segfaut sur gcc 10.2 sur godbolt. J'essaie de construire gcc 10.2 sur mon PC pour comprendre pourquoi, si ce n'est pas un bogue du compilateur. Mettra à jour quand je le déboguerai.


2 commentaires

Donc, si je suis, vous utilisez des modèles variadiques pour que OuterBase<T...> charge send() pour chaque type répertorié dans le modèle grâce à plusieurs couches d'héritage, puis vous utilisez OuterInnerProxy <T ... > pour implémenter toutes les surcharges send() une par une à travers plusieurs couches d'héritage également? Et puis, à la fin, dans OuterDerived, puisque vous en héritez, vous pouvez remplacer l'une des surcharges. C'est ça l'idée? Cela semble fonctionner, mais je ne sais pas quel serait l'impact de tous ces héritages de couches. Aussi si c'est difficile à comprendre, mes collègues me détesteront ...


La surcharge d'exécution sera négligeable - les structures sont toutes aplaties en octets et en décalages à la fin, et vous utilisez déjà des méthodes virtuelles. Les temps de compilation augmenteront avec toutes les manigances de modèles. Votre dernier point est très valable - le code "délicat" n'est pas très bon si personne ne peut le lire, et le passe-partout peut être moins nocif que l'alternative. Vous pouvez également aller entre les deux - par exemple, si vous doSend le code qui déduit quels types ont une surcharge doSend c'est un peu plus passe-partout et un peu moins complexe.



0
votes

Vous n'avez pas réellement besoin de dupliquer du code dans OuterDerived , il suffit que toutes les classes Message dérivent d'une base commune.

template <class Message>
class MessageWrapper : public MessageBase
{
public:
   void sendme(InnerBase* innerBase) override {
      // this chooses the correct overload
      innerBase->send(message);
   }
 private:
   Message message;
};

Il s'agit d'une technique standard de double envoi. sendme doit être virtuel.

class MessageBase {
public:
   virtual void sendme(InnerBase* innerBase) = 0;
};

class MessageA : public MessageBase {
public:
   void sendme(InnerBase* innerBase) override {
      // this chooses the correct overload
      innerBase->send(*this);
   }
};

Maintenant, messageA n'a pas besoin d'être un message réel, cela pourrait être un wrapper autour d'un, et ce wrapper peut être un modèle:

class InnerBase
{
public:
    virtual void doSend(Message* m) = 0;
};

class OuterBase : public InerBase
{
public:
    void doSend(MessageBase* m) override {
        m->sendme(innerBase);
    }
private:
    InnerBase* innerBase;
};

Désormais, doSend est indépendant du type de message réel, les messages ne savent rien des classes internes ou externes, les classes externes ne savent rien des messages spécifiques, et seules les classes innet (chacune d'entre elles) conservent la connaissance des messages spécifiques (chacun d'eux). Vous avez donc toujours besoin send fonctions d' send NxM, mais exactement au même endroit, dans la hiérarchie des classes internes.


0 commentaires