positiva - ¿Cuándo es la covarianza de C++ la mejor solución?
covarianza y correlacion (6)
A menudo me encuentro usando la covarianza cuando trabajo con el código existente para deshacerme de static_casts. Usualmente la situación es similar a esto:
class IPart {};
class IThing {
public:
virtual IPart* part() = 0;
};
class AFooPart : public IPart {
public:
void doThis();
};
class AFooThing : public IThing {
virtual AFooPart* part() {...}
};
class ABarPart : public IPart {
public:
void doThat();
};
class ABarThing : public IThing {
virtual ABarPart* part() {...}
};
Esto me permite
AFooThing* pFooThing = ...;
pFooThing->Part()->doThis();
y
ABarThing pBarThing = ...;
pBarThing->Part()->doThat();
en lugar de
static_cast< AFooPart >(pFooThing->Part())->doThis();
y
static_cast< ABarPart >(pBarThing->Part())->doThat();
Ahora, al encontrar ese código, se podría discutir sobre el diseño original y si existe uno mejor, pero en mi experiencia, a menudo existen limitaciones tales como prioridades, costo / beneficio, etc., que interfieren con la amplia embellecimiento del diseño y permiten solo pequeños pasos como como este
Esta pregunta se hizo aquí hace unas horas y me hizo darme cuenta de que nunca he usado tipos de devolución covariantes en mi propio código. Para aquellos que no están seguros de qué es la covarianza, está permitiendo que el tipo de retorno de (típicamente) las funciones virtuales difiera siempre que los tipos sean parte de la misma jerarquía de herencia. Por ejemplo:
struct A {
virtual ~A();
virtual A * f();
...
};
struct B : public A {
virtual B * f();
...
};
Los diferentes tipos de retorno de las dos funciones f () se dice que son covariantes. Las versiones anteriores de C ++ requerían que los tipos de retorno fueran los mismos, por lo que B tendría que verse así:
struct B : public A {
virtual A * f();
...
};
Entonces, mi pregunta: ¿Alguien tiene un ejemplo del mundo real en el que se requieran tipos de funciones virtuales de retorno covariante, o produzca una solución superior para simplemente devolver un puntero base o referencia?
Creo que la covarianza puede ser útil al declarar métodos de fábrica que devuelven una clase específica y no su clase base. Este artículo explica este escenario bastante bien e incluye el siguiente ejemplo de código:
class product
{
...
};
class factory
{
public:
virtual product *create() const = 0;
...
};
class concrete_product : public product
{
...
};
class concrete_factory : public factory
{
public:
virtual concrete_product *create() const
{
return new concrete_product;
}
...
};
El ejemplo canónico es un .clone()
/ .copy()
. Así que siempre puedes hacer
obj = obj->copy();
independientemente de qué tipo de obj es.
Edición: este método de clonación se definiría en la clase base del objeto (como en realidad está en Java). Entonces, si el clon no fuera covariante, tendría que convertir o estaría restringido a métodos de la clase base raíz (que tendría muy pocos métodos, comparó la clase del objeto fuente de la copia).
En general, la covarianza le permite expresar más información en la interfaz de clase derivada de lo que es cierto en la interfaz de clase base. El comportamiento de una clase derivada es más específico que el de una clase base, y la covarianza expresa (un aspecto de) la diferencia.
Es útil cuando tiene jerarquías relacionadas de gubbins, en situaciones donde algunos clientes querrán usar una interfaz de clase base, pero otros clientes usarán la interfaz de clase derivada. Con const-corrección omitida:
class URI { /* stuff */ };
class HttpAddress : public URI {
bool hasQueryParam(string);
string &getQueryParam(string);
};
class Resource {
virtual URI &getIdentifier();
};
class WebPage : public Resource {
virtual HttpAddress &getIdentifier();
};
Los clientes que saben que tienen una página web (navegadores, tal vez) saben que es significativo mirar los parámetros de consulta. Los clientes que están usando la clase base de recursos no saben tal cosa. Siempre vincularán la HttpAddress&
a una URI&
variable o temporal.
Si sospechan, pero no saben, que su objeto Resource tiene una HttpAddress, entonces pueden dynamic_cast
. Pero la covarianza es superior a "solo saber" y hacer el reparto por la misma razón por la que la escritura estática es útil.
Hay alternativas: pegar la función getQueryParam
en la URI
pero hacer que hasQueryParam
devuelva false para todo (desordena la interfaz URI). Deje WebPage::getIdentifier
definido para devolver la URL&
, devolviendo realmente un HttpIdentifier&
, y HttpIdentifier&
personas que llaman que realicen un dynamic_cast
sin dynamic_cast
( dynamic_cast
el código de llamada y la documentación de WebPage donde dice que "se garantiza que la URL devuelta se puede convertir dinámicamente en HttpAddress") . Agregue una función getHttpIdentifier
a WebPage
(desordena la interfaz de WebPage
). O simplemente use la covarianza para lo que está destinado a hacer, que es el hecho de que una WebPage
FtpAddress
no tiene una dirección FtpAddress
o una dirección de MailtoAddress
, sino una dirección HttpAddress
.
Finalmente, hay, por supuesto, un argumento razonable de que no debería tener jerarquías de gubbins, y mucho menos jerarquías relacionadas de gubbins. Pero esas clases podrían ser simplemente interfaces con métodos virtuales puros, así que no creo que afecte la validez de usar covarianza.
Otro ejemplo es una fábrica de concreto que devolvería punteros a las clases concretas en lugar de la abstracta (lo he usado para uso interno en la fábrica cuando la fábrica tuvo que construir objetos compuestos).
Se vuelve útil en el escenario en el que desea utilizar una fábrica de concreto para generar productos concretos. Siempre quieres la interfaz más especializada que sea lo suficientemente general ...
El código que utiliza la fábrica de hormigón puede suponer con seguridad que los productos son productos concretos, por lo que puede utilizar de forma segura las extensiones proporcionadas por la clase de productos concretos con respecto a la clase abstracta. Esto podría ser considerado como azúcar sintáctica, pero es dulce de todos modos.