Herencia
El punto principal de la herencia es compartir una interfaz y un protocolo comunes entre muchas implementaciones diferentes, de modo que una instancia de una clase derivada se pueda tratar de manera idéntica a cualquier otra instancia desde cualquier otro tipo derivado.
En la herencia de C ++ también trae detalles de implementación, marcar (o no marcar) el destructor como virtual es uno de esos detalles de implementación.
Enlace de funciones
Ahora, cuando se llama a una función, o cualquiera de sus casos especiales, como un constructor o destructor, el compilador debe elegir a qué implementación de función se refería. Entonces debe generar un código de máquina que siga esta intención.
La forma más sencilla de resolver esto sería seleccionar la función en tiempo de compilación y emitir el código de máquina suficiente para que, independientemente de cualquier valor, cuando se ejecute ese fragmento de código, siempre ejecute el código de la función. Esto funciona muy bien, excepto por la herencia.
Si tenemos una clase base con una función (podría ser cualquier función, incluido el constructor o el destructor) y su código llama a una función, ¿qué significa esto?
Tomando de su ejemplo, si llamó initialize_vector()
al compilador tiene que decidir si realmente quiso llamar a la implementación que se encuentra Base
, o la implementación que se encuentra en Derived
. Hay dos formas de decidir esto:
- El primero es decidir que, debido a que llamó desde un
Base
tipo, quiso decir la implementación en Base
.
- El segundo es decidir que, debido a que el tipo de tiempo de ejecución del valor almacenado en el
Base
valor escrito podría ser Base
, o Derived
que la decisión sobre qué llamada hacer, debe tomarse en tiempo de ejecución cuando se llama (cada vez que se llama).
El compilador en este punto está confundido, ambas opciones son igualmente válidas. Esto es cuando virtual
entra en la mezcla. Cuando esta palabra clave está presente, el compilador elige la opción 2 retrasando la decisión entre todas las implementaciones posibles hasta que el código se ejecute con un valor real. Cuando esta palabra clave está ausente, el compilador elige la opción 1 porque ese es el comportamiento normal.
El compilador aún podría elegir la opción 1 en el caso de una llamada de función virtual. Pero solo si puede probar que este es siempre el caso.
Constructores y Destructores
Entonces, ¿por qué no especificamos un constructor virtual?
Más intuitivamente, ¿cómo elegiría el compilador entre implementaciones idénticas del constructor para Derived
y Derived2
? Esto es bastante simple, no puede. No existe un valor preexistente del cual el compilador pueda aprender lo que realmente se pretendía. No hay un valor preexistente porque ese es el trabajo del constructor.
Entonces, ¿por qué necesitamos especificar un destructor virtual?
Más intuitivamente, ¿cómo elegiría el compilador entre implementaciones para Base
y Derived
? Son solo llamadas de función, por lo que ocurre el comportamiento de la llamada de función. Sin un destructor virtual declarado, el compilador decidirá vincularse directamente al Base
destructor independientemente del tipo de tiempo de ejecución de los valores.
En muchos compiladores, si el derivado no declara ningún miembro de datos, ni hereda de otros tipos, el comportamiento en el ~Base()
será adecuado, pero no está garantizado. Funcionaría puramente por casualidad, al igual que pararse frente a un lanzallamas que aún no se había encendido. Estás bien por un tiempo.
La única forma correcta de declarar cualquier tipo de base o interfaz en C ++ es declarar un destructor virtual, de modo que se llame al destructor correcto para cualquier instancia dada de la jerarquía de tipos de ese tipo. Esto permite que la función con el mayor conocimiento de la instancia limpie esa instancia correctamente.
~derived()
que delega al destructor de vec. Alternativamente, está asumiendo queunique_ptr<base> pt
conocería el destructor derivado. Sin un método virtual, este no puede ser el caso. Si bien un unique_ptr puede recibir una función de eliminación que es un parámetro de plantilla sin ninguna representación de tiempo de ejecución, y esa característica no sirve para este código.