Palabra clave "virtual" de C ++ para funciones en clases derivadas. ¿Es necesario?

221

Con la definición de estructura dada a continuación ...

struct A {
    virtual void hello() = 0;
};

Enfoque n. ° 1:

struct B : public A {
    virtual void hello() { ... }
};

Enfoque # 2:

struct B : public A {
    void hello() { ... }
};

¿Hay alguna diferencia entre estas dos formas de anular la función hello?

Anarki
fuente
65
En C ++ 11 puede escribir "void hello () override {}" para declarar explícitamente que está anulando un método virtual. El compilador fallará si no existe un método virtual base, y tiene la misma legibilidad que colocar "virtual" en la clase descendiente.
ShadowChaser
En realidad, en C ++ 11 de gcc, escribir void hello () override {} en la clase derivada está bien porque la clase base ha especificado que el método hello () es virtual. En otras palabras, el uso de la palabra virtual en la clase derivada no es necesario / obligatorio, de todos modos para gcc / g ++. (Estoy usando gcc versión 4.9.2 en un RPi 3) Pero de todos modos es una buena práctica incluir la palabra clave virtual en el método de la clase derivada.
Será el

Respuestas:

183

Son exactamente lo mismo. No hay diferencia entre ellos aparte de que el primer enfoque requiere más tipeo y es potencialmente más claro.

James McNellis
fuente
25
Esto es cierto, pero la Guía de portabilidad de Mozilla C ++ recomienda usar siempre virtual porque "algunos compiladores" emiten advertencias si no lo hace. Lástima que no mencionen ningún ejemplo de tales compiladores.
Sergei Tachenov
55
También agregaría que marcarlo explícitamente como virtual ayudará a recordarle que también haga que el destructor sea virtual.
lfalin
1
Solo para mencionar, lo mismo aplicable al destructor virtual
Atul
66
@SergeyTachenov según el comentario de clifford a su propia respuesta , el ejemplo de tales compiladores es armcc.
Ruslan
44
@Rasmi, la nueva guía de portabilidad está aquí , pero ahora recomienda usar la overridepalabra clave.
Sergei Tachenov
83

La 'virtualidad' de una función se propaga implícitamente, sin embargo, al menos un compilador que uso generará una advertencia si la virtualpalabra clave no se usa explícitamente, por lo que es posible que desee usarla solo para mantener el compilador en silencio.

Desde un punto de vista puramente estilístico, incluir la virtualpalabra clave claramente 'anuncia' el hecho al usuario de que la función es virtual. Esto será importante para cualquier persona que subclasifique B sin tener que verificar la definición de A. Para jerarquías de clase profundas, esto se vuelve especialmente importante.

Clifford
fuente
12
¿Qué compilador es este?
James McNellis
35
@James: armcc (compilador de ARM para dispositivos ARM)
Clifford
55

La virtualpalabra clave no es necesaria en la clase derivada. Aquí está la documentación de respaldo, del C ++ Draft Standard (N3337) (el énfasis es mío):

10.3 Funciones virtuales

2 Si una función miembro virtual vfse declara en una clase Basey en una clase Derived, derivada directa o indirectamente de Base, una función miembro vfcon el mismo nombre, lista de tipos de parámetros (8.3.5), calificación cv y calificador ref ( o ausencia de lo mismo) como Base::vfse declara, entonces Derived::vftambién es virtual ( ya sea que se declare o no ) y se anula Base::vf.

R Sahu
fuente
55
Esta es, de lejos, la mejor respuesta aquí.
Fantástico Mr Fox el
33

No, el virtual no se requiere palabra clave en la anulación de la función virtual de las clases derivadas. Pero vale la pena mencionar una trampa relacionada: una falla al anular una función virtual.

El error de anulación se produce si tiene la intención de anular una función virtual en una clase derivada, pero comete un error en la firma para que declare una función virtual nueva y diferente. Esta función puede ser una sobrecarga de la función de clase base, o puede diferir en nombre. Si usa o no elvirtual palabra clave en la declaración de función de clase derivada, el compilador no podrá decir que tenía la intención de anular una función de una clase base.

Sin embargo, esta trampa es abordada afortunadamente por la función de lenguaje de anulación explícita de C ++ 11 , que permite que el código fuente especifique claramente que una función miembro tiene la intención de anular una función de clase base:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

El compilador emitirá un error en tiempo de compilación y el error de programación será inmediatamente obvio (quizás la función en Derivado debería haber tomado a floatcomo argumento).

Consulte WP: C ++ 11 .

Colin D Bennett
fuente
11

Agregar la palabra clave "virtual" es una buena práctica ya que mejora la legibilidad, pero no es necesario. Las funciones declaradas virtuales en la clase base y tener la misma firma en las clases derivadas se consideran "virtuales" de forma predeterminada.

Sujay Ghosh
fuente
7

No hay diferencia para el compilador, cuando escribe el virtualen la clase derivada o lo omite.

Pero debe mirar la clase base para obtener esta información. Por lo tanto, recomendaría agregar la virtualpalabra clave también en la clase derivada, si desea mostrarle al ser humano que esta función es virtual.

harper
fuente
2

Hay una diferencia considerable cuando tiene plantillas y comienza a tomar clases básicas como parámetros de plantilla:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

La parte divertida es que ahora puede definir funciones de interfaz y no de interfaz más adelante para definir clases. Eso es útil para interfuncionar interfaces entre bibliotecas (no confíe en esto como un proceso de diseño estándar de una sola biblioteca). No le cuesta nada permitir esto para todas sus clases; incluso podría typedefB a algo si lo desea.

Tenga en cuenta que, si hace esto, es posible que también desee declarar copiar / mover constructores como plantillas: permitir construir desde diferentes interfaces le permite 'lanzar' entre diferentes B<>tipos.

Es cuestionable si debe agregar soporte para const A&in t_hello(). La razón habitual para esta reescritura es alejarse de la especialización basada en herencia a una basada en plantilla, principalmente por razones de rendimiento. Si continúa admitiendo la interfaz anterior, difícilmente podrá detectar (o disuadir) el uso anterior.

lorro
fuente
1

La virtualpalabra clave debe agregarse a las funciones de una clase base para hacerlas reemplazables. En su ejemplo, struct Aes la clase base. virtualno significa nada para usar esas funciones en una clase derivada. Sin embargo, si desea que su clase derivada también sea una clase base en sí misma, y ​​desea que esa función sea reemplazable, entonces tendría que poner virtualallí.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Aquí Chereda de B, por Blo que no es la clase base (también es una clase derivada), y Ces la clase derivada. El diagrama de herencia se ve así:

A
^
|
B
^
|
C

Por lo tanto, debe colocar las virtualfunciones delante de las clases base potenciales que pueden tener hijos. virtualpermite que sus hijos anulen sus funciones. No hay nada de malo en poner las virtualfunciones delante de las clases derivadas, pero no es obligatorio. Sin embargo, se recomienda, porque si alguien quisiera heredar de su clase derivada, no estaría contento de que la anulación del método no funcione como se esperaba.

Así que ponga virtualdelante de las funciones en todas las clases involucradas en la herencia, a menos que sepa con certeza que la clase no tendrá hijos que necesiten anular las funciones de la clase base. Es una buena práctica.

Galaxia
fuente
0

Ciertamente incluiré la palabra clave virtual para la clase secundaria, porque

  • yo. Legibilidad.
  • ii) Esta clase secundaria puede derivarse más abajo, no desea que el constructor de la clase derivada adicional llame a esta función virtual.
usuario2264698
fuente
1
Creo que quiere decir que sin marcar la función secundaria como virtual, un programador que se deriva de la clase secundaria más adelante puede no darse cuenta de que la función en realidad es virtual (porque nunca miró la clase base) y podría llamarla durante la construcción ( que puede o no hacer lo correcto).
PfhorSlayer