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; };
où 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()
...
3 Réponses :
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.
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 quelsTs
dansOuterBase<Ts...>
ont des surcharges dansInnerBase
. Un moyen simple de le faire est d'utiliser SFINAE pour convertir chaque type en un tuple plein ou vide, puis de les écraser avecstd::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 uneOuterBase
et uneInnerBase*
. 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.
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.
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.
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
etBlue
, et deux classes de messageSoft
etHard
, avez-vous vraiment besoin de quatre implémentationssendMessage
nonsendMessage
, 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.