c++ superclass subclassing idiomatic

¿Qué es lo más parecido en C++ a la definición retroactiva de una superclase de una clase definida?



superclass subclassing (5)

Sé que C ++ no tiene el mecanismo opuesto: "superclase de una clase conocida"

Oh sí, lo hace:

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

y ya te vas Todo en tiempo de compilación, no hace falta decirlo.

Si tiene una class A que no se puede cambiar y necesita ubicarla en una estructura de herencia, use algo en las líneas de

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

Tenga en cuenta que dynamic_cast alcanzará los punteros A* dados los punteros Superclass* y viceversa. Ditto Class* punteros. En este punto, te estás acercando a Composition , Traits y Concepts .

Supongamos que tengo la clase

class A { protected: int x,y; double z,w; public: void foo(); void bar(); void baz(); };

definido y utilizado en mi código y el código de otros. Ahora, quiero escribir una biblioteca que podría funcionar muy bien en las letras A, pero en realidad es más general y podría operar en:

class B { protected: int y; double z; public: void bar(); };

y quiero que mi biblioteca sea general, así que defino una clase B y eso es lo que toman sus API.

Me gustaría poder decirle al compilador, no en la definición de A que ya no controlo, sino en otra parte, probablemente en la definición de B:

Mira, por favor intenta pensar en B como una superclase de A Por lo tanto, en particular, colóquelo en la memoria para que si reinterprete un A* como un B* , mi código esperando que B* s funcione. Y, por favor, acepte A* como B* (y A& como B& etc.).

En C ++ podemos hacer esto de otra manera, es decir, si B es la clase que no controlamos, podemos realizar una operación de "subclase de una clase conocida" con class A : public B { ... } ; y sé que C ++ no tiene el mecanismo opuesto: "superclase, una clase conocida A por una nueva clase B". Mi pregunta es: ¿cuál es la aproximación más alcanzable de este mecanismo?

Notas:

  • Todo esto es estrictamente en tiempo de compilación, no en tiempo de ejecución.
  • No puede haber ningún cambio en la class A Solo puedo modificar la definición de B y el código que conoce tanto A como B Otras personas seguirán usando la clase A , y yo también, si quiero que mi código interactúe con el de ellos.
  • Esto debería ser preferiblemente "escalable" a múltiples superclases. Entonces tal vez también tengo class C { protected: int x; double w; public: void baz(); } class C { protected: int x; double w; public: void baz(); } class C { protected: int x; double w; public: void baz(); } que también debe comportarse como una superclase de A

Esto parece más como un polimorfismo estático bastante dinámico. Como @ ZdeněkJelínek ya ha mencionado, usted podría tener una plantilla para asegurar que se transmita la interfaz adecuada, todo durante el tiempo de compilación.

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"); }

Esto debería hacer lo que le gustaría (es decir, usar la barra para cualquier clase) sin tener que recurrir a una búsqueda de vtable y sin tener la capacidad de modificar las clases. Este nivel de direccionamiento indirecto debe estar delimitado con los indicadores de optimización adecuados establecidos. En otras palabras, tendrá la eficiencia en tiempo de ejecución de invocar directamente la barra.


Las plantillas normales hacen esto, y el compilador te informará cuando las uses incorrectamente.

en lugar 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; } }

usted escribe

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; } }

Y usas BConsumer1 y BConsumer2 como

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

Y tendría BSubclass<A> y BSubclass<B> , como tipos que usan la interfaz B para hacer algo.


No hay manera de cambiar el comportamiento de una clase sin cambiar la clase. De hecho, no hay ningún mecanismo para agregar una clase padre después de que A ya se haya definido.

Solo puedo modificar la definición de B y el código que conoce tanto A como B.

No puede cambiar A , pero puede cambiar el código que usa A Entonces, podrías, en lugar de usar A , simplemente usar otra clase que hereda de B (llamémoslo D ). Creo que este es el más alcanzable del mecanismo deseado.

D puede reutilizar A como un subobjeto (posiblemente como una base) si eso es útil.

Esto preferiblemente debe ser "escalable" a múltiples superclases.

D puede heredar tantas superclases como lo necesite.

Una demostración

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

Ahora D comporta exactamente como A se comportaría si solo A hubiera heredado B y C

Heredar A públicamente permitiría deshacerse de toda la placa de la delegación:

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

Sin embargo, creo que podría tener la posibilidad de crear confusión entre el código que usa A sin el conocimiento de B y el código que usa el conocimiento de B (y por lo tanto usa D ). El código que usa D puede lidiar fácilmente con A , pero no al revés.

No heredar A en absoluto, pero usar un miembro en su lugar le permitiría no copiar A para crear D , sino que se refiere a uno existente:

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

Obviamente, esto tiene el potencial de errores con la duración de los objetos. Eso podría resolverse con punteros compartidos:

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();} };

Sin embargo, esto es presumiblemente solo una opción si el otro código que no sabe acerca de B o D también usa punteros compartidos.


Puedes hacer lo siguiente:

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(); } };

La idea es utilizar el borrado de tipos (es decir, las clases Interface y Interfacer<T> ) debajo de las coberturas para permitir que C tome cualquier cosa a la que pueda llamar bar y luego su biblioteca tomará objetos de tipo C