c++ - Pimpl idioma con herencia
inheritance oop (5)
Como dijo stefan.ciobaca, si realmente quieres que A sea extensible, querrás que pAImpl
esté protegido.
Sin embargo, su definición en B
de la void bar(){pAImpl->bar();};
parece extraño, ya que la bar
es un método en BImpl
y no en AImpl
.
Hay al menos tres alternativas fáciles que evitarían ese problema:
- Tu alternativa (3).
- Una variación en (3) en la cual
BImpl
extiendeAImpl
(heredando la implementación existente defoo
lugar de definir otro),BImpl
define labar
, yB
usa su privadoBImpl* pBImpl
para acceder a ambos. - Delegación, en la que
B
contiene punteros privados para cada uno de losAImpl
yBImpl
y reenvía cadafoo
ybar
al implementador apropiado.
Quiero usar idioma de pimpl con herencia.
Aquí está la clase pública base y su clase de implementación:
class A
{
public:
A(){pAImpl = new AImpl;};
void foo(){pAImpl->foo();};
private:
AImpl* pAImpl;
};
class AImpl
{
public:
void foo(){/*do something*/};
};
Y quiero ser capaz de crear la clase pública derivada con su clase de implementación:
class B : public A
{
public:
void bar(){pAImpl->bar();}; // Can''t do! pAimpl is A''s private.
};
class BImpl : public AImpl
{
public:
void bar(){/*do something else*/};
};
Pero no puedo usar pAimpl en B porque es privado de A.
Entonces veo algunas formas de resolverlo:
- Cree el miembro BImpl * pBImpl en B y páselo a A con el constructor adicional A, A (AImpl *).
- Cambie pAImpl para estar protegido (o agregue una función Get) y úselo en B.
- B no debe heredar de A. Cree el miembro BImpl * pBImpl en B, y cree foo () y bar () en B, que usará pBImpl.
- ¿Cualquier otra manera?
¿Qué debería elegir?
Haría (1) porque los privados de A son para B.
En realidad, no lo pasaría a A como sugieres, porque A hace propio en A :: A (). Llamar a pApimpl->whatever()
de Bis también no apropiado (privado significa privado).
La forma correcta es hacer (2).
En general, probablemente debería considerar hacer todas las variables miembro protegidas por defecto en lugar de privadas.
La razón por la cual la mayoría de los programadores eligen lo privado es que no piensan en otros que quieren derivar de su clase y la mayoría de los manuales introductorios de C ++ enseñan este estilo, en el sentido de que todos los ejemplos usan privado.
EDITAR
La duplicación de código y la asignación de memoria son efectos secundarios indeseados del uso del patrón de diseño de proxeneta y no puedo evitarlo, que yo sepa.
Si necesita que Bimpl herede Aimpl y desea exponerles una interfaz consistente a través de A y B, B también necesitaría heredar A.
Una cosa que puede hacer para simplificar las cosas en este escenario es que B herede de A y solo cambie el contructor de manera que B :: B (...) {} cree un Bimpl y agregue despachos para todos los métodos de Bimpl que son no en Aimpl.
class A
{
public:
A(bool DoNew = true){
if(DoNew)
pAImpl = new AImpl;
};
void foo(){pAImpl->foo();};
protected:
void SetpAImpl(AImpl* pImpl) {pAImpl = pImpl;};
private:
AImpl* pAImpl;
};
class AImpl
{
public:
void foo(){/*do something*/};
};
class B : public A
{
public:
B() : A(false){
pBImpl = new BImpl;
SetpAImpl(pBImpl);
};
void bar(){pBImpl->bar();};
private:
BImpl* pBImpl;
};
class BImpl : public AImpl
{
public:
void bar(){/*do something else*/};
};
Creo que la mejor manera desde una perspectiva puramente orientada a objetos es no hacer que BImpl herede de AImpl (¿es eso lo que quisiste decir en la opción 3?). Sin embargo, tener BImpl derivado de AImpl (y pasar la impl deseada a un constructor de A) también está bien, siempre que la variable miembro pimpl sea const
. Realmente no importa si usa una función get o accede directamente a la variable desde las clases derivadas, a menos que desee imponer const-correctness en las clases derivadas. Permitir que las clases derivadas cambien pimpl no es una buena idea, podrían arruinar toda la inicialización de A, y tampoco es una buena idea dejar que la clase base lo cambie. Considere esta extensión a su ejemplo:
class A
{
protected:
struct AImpl {void foo(); /*...*/};
A(AImpl * impl): pimpl(impl) {}
AImpl * GetImpl() { return pimpl; }
const AImpl * GetImpl() const { return pimpl; }
private:
AImpl * pimpl;
public:
void foo() {pImpl->foo();}
friend void swap(A&, A&);
};
void swap(A & a1, A & a2)
{
using std::swap;
swap(a1.pimpl, a2.pimpl);
}
class B: public A
{
protected:
struct BImpl: public AImpl {void bar();};
public:
void bar(){static_cast<BImpl *>(GetImpl())->bar();}
B(): A(new BImpl()) {}
};
class C: public A
{
protected:
struct CImpl: public AImpl {void baz();};
public:
void baz(){static_cast<CImpl *>(GetImpl())->baz();}
C(): A(new CImpl()) {}
};
int main()
{
B b;
C c;
swap(b, c); //calls swap(A&, A&)
//This is now a bad situation - B.pimpl is a CImpl *, and C.pimpl is a BImpl *!
//Consider:
b.bar();
//If BImpl and CImpl weren''t derived from AImpl, then this wouldn''t happen.
//You could have b''s BImpl being out of sync with its AImpl, though.
}
Aunque es posible que no tenga una función swap (), puede concebir fácilmente que se produzcan problemas similares, particularmente si A es asignable, ya sea por accidente o por intención. Es una violación algo sutil del principio de sustituibilidad de Liskov. Las soluciones son:
No cambie los miembros pimpl después de la construcción. Declare que son
AImpl * const pimpl
. Entonces, los constructores derivados pueden pasar un tipo apropiado y el resto de la clase derivada puede descender con confianza. Sin embargo, entonces no puede, por ejemplo, realizar intercambios, asignaciones o copias sin escritura, ya que estas técnicas requieren que pueda cambiar el miembro pimpl. Sin embargo, sin embargo, probablemente no tenga la intención de hacer estas cosas si tiene una jerarquía de herencia.Tener clases AImpl y BImpl no relacionadas (y tontas) para las variables privadas de A y B, respectivamente. Si B quiere hacer algo con A, entonces usa la interfaz pública o protegida de A. Esto también conserva la razón más común para usar pimpl: ser capaz de ocultar la definición de AImpl en un archivo cpp que las clases derivadas no pueden usar, por lo que la mitad de su programa no necesita recompilar cuando cambia la implementación de A.