31 votes

Quelle est la chose la plus proche en C++ de la définition rétroactive d'une superclasse d'une classe définie ?

Supposons que j'ai la classe

class A {
protected:
    int x,y;
    double z,w;

public:
    void foo();
    void bar();
    void baz();
};

définis et utilisés dans mon code et celui des autres. Maintenant, je veux écrire une bibliothèque qui pourrait très bien opérer sur les A, mais qui est en fait plus générale, et qui serait capable d'opérer sur :

class B {
protected:
    int y;
    double z;

public:
 void bar();
};

et je veux que ma bibliothèque soit générale, donc je définis une classe B et c'est ce que prennent ses API.

J'aimerais pouvoir le dire au compilateur - pas dans la définition de A que je ne contrôle plus, mais ailleurs, probablement dans la définition de B :

Ecoutez, s'il vous plaît, essayez de penser à B comme une superclasse de A . Ainsi, en particulier, le mettre en mémoire de sorte que si je réinterprète une A* en tant que B* mon code attend B* fonctionneraient. Et s'il vous plaît, acceptez réellement A* en tant que B* (et A& en tant que B& etc.).

En C++, nous pouvons le faire dans l'autre sens, c'est-à-dire que si B est la classe que nous ne contrôlons pas, nous pouvons effectuer une opération "sous-classe une classe connue" avec class A : public B { ... } ; et je sais que le C++ n'a pas le mécanisme inverse - "superclasser une classe A connue par une nouvelle classe B". Ma question est la suivante : quelle est l'approximation la plus proche de ce mécanisme ?

Notes :

  • Tout ceci est strictement du ressort de la compilation, et non de l'exécution.
  • Il peut y avoir pas de des changements de quelque nature que ce soit class A . Je ne peux que modifier la définition de B et le code qui connaît les deux A et B . D'autres personnes continueront à utiliser la classe A et je le ferai aussi si je veux que mon code interagisse avec le leur.
  • Cela devrait de préférence être "extensible" à plusieurs superclasses. Alors peut-être que j'ai aussi class C { protected: int x; double w; public: void baz(); } qui devrait également se comporter comme une superclasse de A .

28voto

GuyGreer Points 4240

Vous pouvez procéder comme suit :

class C
{
  struct Interface
  {
    virtual void bar() = 0;
    virtual ~Interface(){}
  };

  template <class T>
  struct Interfacer : Interface
  {
    T t;
    Interfacer(T t):t(t){}
    void bar() { t.bar(); }
  };

  std::unique_ptr<Interface> interface;

  public:
    template <class T>
    C(const T & t): interface(new Interfacer<T>(t)){}
    void bar() { interface->bar(); }
};

L'idée est d'utiliser l'effacement de type (c'est le principe de l'effacement de type). Interface et Interfacer<T> classes) sous les couvertures pour permettre C pour prendre tout ce que vous pouvez appeler bar on et alors votre bibliothèque prendra des objets de type C .

15voto

Bathsheba Points 23209

Je sais que le C++ n'a pas le mécanisme inverse - "superclasse une classe connue". connue".

Oh oui, c'est vrai :

template <class Superclass>
class Class : public Superclass
{    
};

et c'est parti. Tout cela au moment de la compilation, il va sans dire.


Si vous avez un class A qui ne peut pas être modifié et que vous devez insérer dans une structure d'héritage, utilisez quelque chose du type

template<class Superclass>
class Class : public A, public Superclass
{
};

Notez que dynamic_cast atteindra A* pointeurs donnés Superclass* et vice-versa. Idem Class* pointeurs. A ce stade, vous vous rapprochez de Composition , Traits et Concepts .

5voto

Caleth Points 17517

Les modèles normaux font cela, et le compilateur vous informe lorsque vous les utilisez de manière incorrecte.

au lieu de

void BConsumer1(std::vector<B*> bs)
{ std::for_each(bs.begin(), bs.end(), &B::bar); }

void BConsumer2(B& b)
{ b.bar(); }

class BSubclass : public B 
{
    double xplusz() const { return B::x + B::z; }
}

vous écrivez

template<typename Blike>
void BConsumer1(std::vector<Blike*> bs)
{ std::for_each(bs.begin(), bs.end(), &Blike::bar); }

template<typename Blike>
void BConsumer2(Blike& b)
{ b.bar(); }

template<typename Blike>
class BSubclass : public Blike 
{
    double xplusz() const { return Blike::x + Blike::z; }
}

Et vous utilisez BConsumer1 & BConsumer2 comme suit

std::vector<A*> as = /* some As */
BConsumer1(as); // deduces to BConsumer1<A>
A a;
BConsumer2(a); // deduces to BConsumer2<A>

std::vector<B*> bs = /* some Bs */
BConsumer1(bs); // deduces to BConsumer1<B>
// etc

Et vous auriez BSubclass<A> et BSubclass<B> comme les types qui utilisent l'option B l'interface pour faire quelque chose.

4voto

user2079303 Points 4916

Il n'y a aucun moyen de modifier le comportement d'une classe sans modifier la classe. Il n'existe en effet aucun mécanisme permettant d'ajouter une classe parente après la classe A a déjà été défini.

Je peux seulement modifier la définition de B et un code qui connaît à la fois A et B .

Vous ne pouvez pas changer A mais vous pouvez modifier le code qui utilise A . Vous pourriez donc, au lieu d'utiliser A il suffit d'utiliser une autre classe qui hérite de B (appelons-la D ). Je pense que c'est ce qui se rapproche le plus du mécanisme souhaité.

D peut réutiliser A comme sous-objet (éventuellement comme base) si cela est utile.

Il est préférable que cela soit "extensible" à plusieurs superclasses.

D peut hériter d'autant de super-classes que nécessaire.

Une démo :

class D : A, public B, public C {
public:
    D(const A&);
    void foo(){A::foo();}
    void bar(){A::bar();}
    void baz(){A::baz();}
};

Maintenant D se comporte exactement comme A se comporterait si seulement A avait hérité B et C .

Héritage de A publiquement permettrait de se débarrasser de tout le boilerplate de la délégation :

class D : public A, public B, public C {
public:
    D(const A&);
};

Cependant, je pense que cela pourrait potentiellement créer une confusion entre le code qui utilise A sans connaissance de B et le code qui utilise les connaissances de B (et utilise donc D ). Le code qui utilise D peut facilement faire face à A mais pas l'inverse.

Ne pas hériter A mais utiliser un membre à la place vous permettrait de ne pas copier A pour créer D mais font plutôt référence à un modèle existant :

class D : public B, public C {
    A& a;
public:
    D(const A&);
    void foo(){a.foo();}
    void bar(){a.bar();}
    void baz(){a.baz();}
};

Cela peut évidemment entraîner des erreurs dans la durée de vie des objets. Cela pourrait être résolu avec des pointeurs partagés :

class D : public B, public C {
    std::shared_ptr<A> a;
public:
    D(const std::shared_ptr<A>&);
    void foo(){a->foo();}
    void bar(){a->bar();}
    void baz(){a->baz();}
};

Cependant, ceci n'est vraisemblablement une option que si l'autre code qui ne connaît pas l'existence de B ou D utilise également des pointeurs partagés.

3voto

Andrew Points 393

Cela ressemble plus à du polymorphisme statique qu'à du dynamique. Comme @ZdeněkJelínek l'a déjà mentionné, vous pourriez utiliser un modèle pour vous assurer que la bonne interface est transmise, le tout pendant la compilation.

namespace details_ {
   template<class T, class=void>
   struct has_bar : std::false_type {};

   template<class T>
   struct has_bar<T, std::void_t<decltype(std::declval<T>().bar())>> : std::true_type {};
}

template<class T>
constexpr bool has_bar = details_::has_bar<T>::value;

template<class T>
std::enable_if_t<has_bar<T>> use_bar(T *t) { t->bar(); }

template<class T>
std::enable_if_t<!has_bar<T>> use_bar(T *) {
   static_assert(false, "Cannot use bar if class does not have a bar member function");
}

Cela devrait permettre de faire ce que vous souhaitez (c'est-à-dire utiliser bar pour n'importe quelle classe) sans avoir à recourir à une consultation de vtable et sans avoir la possibilité de modifier les classes. Ce niveau d'indirection devrait être éliminé par inline avec des drapeaux d'optimisation appropriés. En d'autres termes, vous aurez l'efficacité d'exécution d'une invocation directe de bar.

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