Tipo de retorno de función virtual C ++

Respuestas:

86

En algunos casos, sí, es legal que una clase derivada anule una función virtual usando un tipo de retorno diferente siempre que el tipo de retorno sea covariante con el tipo de retorno original. Por ejemplo, considere lo siguiente:

class Base {
public:
    virtual ~Base() {}

    virtual Base* clone() const = 0;
};

class Derived: public Base {
public:
    virtual Derived* clone() const {
        return new Derived(*this);
    }
};

Aquí, Basedefine una función virtual pura llamada cloneque devuelve un Base *. En la implementación derivada, esta función virtual se reemplaza utilizando un tipo de retorno de Derived *. Aunque el tipo de retorno no es el mismo que en la base, esto es perfectamente seguro porque en cualquier momento escribirías

Base* ptr = /* ... */
Base* clone = ptr->clone();

La llamada a clone()siempre devolverá un puntero a un Baseobjeto, ya que incluso si devuelve a Derived*, este puntero es implícitamente convertible a a Base*y la operación está bien definida.

De manera más general, el tipo de retorno de una función nunca se considera parte de su firma. Puede anular una función miembro con cualquier tipo de retorno siempre que el tipo de retorno sea covariante.

templatetypedef
fuente
9
Este "Puede anular una función miembro con cualquier tipo de retorno" no es correcto. Puede anular siempre que el tipo de retorno sea idéntico o covariante (lo que explicó), punto. No hay un caso más general aquí.
bronekk
1
@ bronekk- El resto de la oración que citó luego establece que el nuevo tipo de retorno debe ser utilizable en cualquier lugar donde se encuentre el tipo original; es decir, el nuevo tipo es covariante con el original.
templatetypedef
que el resto de la oración no es correcta; imagina reemplazar Base*con longy Derived*con int(o al revés, no importa). No funcionará.
bronekk
@ bronekk- ¡Ah sí, no pensé en eso! Gracias por señalar eso.
templatetypedef
1
Que el tipo se puede usar en cualquier lugar donde se encuentre el tipo original y la covarianza son dos contextos diferentes. Covariante significa que la relación entre los tipos que implementan la función y la relación entre los tipos devueltos varía de la misma manera. Contravariante (aunque no es útil en C ++) es el contexto opuesto. Algunos lenguajes permiten argumentos contravariantes en el despacho dinámico (si la base toma un objeto de un tipo T, el tipo derivado puede tomar una T 'donde T' es una base de T - a medida que baja una jerarquía, sube por la otra ).
David Rodríguez - dribeas
53

Si. Se permite que los tipos de retorno sean diferentes siempre que sean covariantes . El estándar C ++ lo describe así (§10.3 / 5):

El tipo de retorno de una función invalidante será idéntico al tipo de retorno de la función invalidada o covariante con las clases de las funciones. Si una función D::fanula una función B::f, el tipo de retorno de las funciones es covariante si satisface los siguientes criterios:

  • ambos son punteros a clases o referencias a clases 98)
  • la clase en el tipo de retorno de B::fes la misma clase que la clase en el tipo de retorno de D::fo, es una clase base directa o indirecta inequívoca de la clase en el tipo de retorno de D::fy es accesible enD
  • ambos punteros o referencias tienen la misma calificación de cv y el tipo de clase en el tipo de retorno D::ftiene la misma calificación de cv o menos que el tipo de clase en el tipo de retorno de B::f.

La nota al pie 98 señala que "no se permiten punteros de varios niveles a clases o referencias a punteros de varios niveles a clases".

En resumen, si Des un subtipo de B, entonces el tipo de retorno de la función en Ddebe ser un subtipo del tipo de retorno de la función en B. El ejemplo más común es cuando los tipos de devolución se basan en Dy B, pero no es necesario que lo estén. Considere esto, donde tenemos dos jerarquías de tipos separadas:

struct Base { /* ... */ };
struct Derived: public Base { /* ... */ };

struct B {
  virtual Base* func() { return new Base; }
  virtual ~B() { }
};
struct D: public B {
  Derived* func() { return new Derived; }
};

int main() {
  B* b = new D;
  Base* base = b->func();
  delete base;
  delete b;
}

La razón por la que esto funciona es porque cualquier persona que llama a funcestá esperando un Basepuntero. Cualquier Basepuntero servirá. Por lo tanto, si D::funcpromete devolver siempre un Derivedpuntero, siempre cumplirá el contrato establecido por la clase antecesora porque cualquier Derivedpuntero se puede convertir implícitamente en un Basepuntero. Por lo tanto, las personas que llaman siempre obtendrán lo que esperan.


Además de permitir que varíe el tipo de retorno, algunos lenguajes también permiten que varíen los tipos de parámetros de la función primordial. Cuando hacen eso, generalmente necesitan ser contravariantes . Es decir, si B::facepta a Derived*, se D::fle permitirá aceptar a Base*. A los descendientes se les permite ser más flexibles en lo que aceptarán y más estrictos en lo que devolverán. C ++ no permite la contravarianza de tipo de parámetro. Si cambia los tipos de parámetros, C ++ lo considera una función completamente nueva, por lo que comienza a sobrecargarse y ocultarse. Para obtener más información sobre este tema, consulte Covarianza y contravarianza (informática) en Wikipedia.

Rob Kennedy
fuente
2
¿Es esta una característica real o un efecto secundario del tipo de retorno que no se usa en la resolución?
Martin York
1
@Martin, definitivamente una característica. Estoy bastante seguro de que la resolución de sobrecarga no tuvo nada que ver con eso. El tipo de retorno se usa si anula una función.
Rob Kennedy