clang: no hay definiciones de métodos virtuales fuera de línea (clase C++ pura y abstracta)
Estoy intentando compilar el siguiente código C++ simple usando Clang-3.5:
prueba.h:
class A
{
public:
A();
virtual ~A() = 0;
};
prueba.cc:
#include "test.h"
A::A() {;}
A::~A() {;}
El comando que uso para compilar esto (Linux, uname -r: 3.16.0-4-amd64):
$clang-3.5 -Weverything -std=c++11 -c test.cc
Y el error que me sale:
./test.h:1:7: warning: 'A' has no out-of-line virtual method definitions; its vtable will be emitted in every translation unit [-Wweak-vtables]
¿Alguna pista de por qué esto emite una advertencia? El destructor virtual no está incluido en absoluto. Todo lo contrario, hay una definición fuera de línea proporcionada en test.cc. ¿Que me estoy perdiendo aqui?
Editar
No creo que esta pregunta sea un duplicado de: ¿
Cuál es el significado de -Wweak-vtables de clang?
como sugirió Filip Roséen. En mi pregunta me refiero específicamente a clases abstractas puras (no mencionadas en el duplicado sugerido). Sé cómo -Wweak-vtables
funcionan las clases no abstractas y estoy bien con eso. En mi ejemplo defino el destructor (que es puramente abstracto) en el archivo de implementación. Esto debería evitar que Clang emita errores, incluso con -Wweak-vtables
.
No queremos colocar la vtable en cada unidad de traducción. Por lo tanto, debe haber algún orden de las unidades de traducción, de modo que podamos decir entonces que colocamos la vtable en la "primera" unidad de traducción. Si este orden no está definido, emitimos la advertencia.
La respuesta la encontrará en el Itanium CXX ABI . En el apartado de mesas virtuales (5.2.3) encuentras:
La tabla virtual para una clase se emite en el mismo objeto que contiene la definición de su función clave, es decir, la primera función virtual no pura que no está en línea en el punto de definición de la clase. Si no hay una función clave, se emite en todos los lugares utilizados. La tabla virtual emitida incluye el grupo de tablas virtuales completo para la clase, cualquier tabla virtual de nueva construcción requerida para los subobjetos y el VTT para la clase. Se emiten en un grupo COMDAT, con el nombre destrozado de la tabla virtual como símbolo identificativo. Tenga en cuenta que si la función clave no se declara en línea en la definición de clase, pero su definición posterior siempre se declara en línea, se emitirá en cada objeto que contenga la definición.
NOTA : En abstracto, se podría utilizar un destructor virtual puro como función clave, ya que debe definirse aunque sea puro. Sin embargo, el comité ABI no se dio cuenta de este hecho hasta que se completó la especificación de la función clave; por lo tanto, un destructor virtual puro no puede ser la función clave .
La segunda sección es la respuesta a tu pregunta. Un destructor virtual puro no es una función clave. Por lo tanto, no está claro dónde colocar la vtable y está colocada en todas partes. Como consecuencia recibimos la advertencia.
Incluso encontrarás esta explicación en la documentación fuente de Clang .
Específicamente a la advertencia: recibirá la advertencia cuando todas sus funciones virtuales pertenezcan a una de las siguientes categorías:
inline
se especificaA::x()
en la definición de clase.struct A { inline virtual void x(); virtual ~A() { } }; void A::x() { }
B::x() está en línea en la definición de clase.
struct B { virtual void x() { } virtual ~B() { } };
C::x() es puramente virtual
struct C { virtual void x() = 0; virtual ~C() { } };
(Pertenece a 3.) Tienes un destructor virtual puro.
struct D { virtual ~D() = 0; }; D::~D() { }
En este caso, se podría definir el orden, porque se debe definir el destructor, sin embargo, por definición, todavía no existe una "primera" unidad de traducción.
Para todos los demás casos, la función clave es la primera función virtual que no encaja en una de estas categorías, y la vtable se colocará en la unidad de traducción donde se define la función clave.
Por un momento, olvidémonos de las funciones virtuales puras e intentemos comprender cómo el compilador puede evitar emitir la vtable en todas las unidades de traducción que incluyen la declaración de una clase polimórfica.
Cuando el compilador ve la declaración de una clase con funciones virtuales, verifica si hay funciones virtuales que solo están declaradas pero no definidas dentro de la declaración de clase. Si existe exactamente una de esas funciones, el compilador sabe con certeza que debe definirse en algún lugar (de lo contrario, el programa no se vinculará) y emite la vtable solo en la unidad de traducción que alberga la definición de esa función. Si hay varias funciones de este tipo, el compilador elige una de ellas utilizando algunos criterios de selección deterministas y, con respecto a la decisión de dónde emitir la vtable, ignora las otras. La forma más sencilla de seleccionar una función virtual representativa única es tomar la primera del conjunto de candidatos, y esto es lo que hace clang.
Entonces, la clave para esta optimización es seleccionar un método virtual de modo que el compilador pueda garantizar que encontrará una (única) definición de ese método en alguna unidad de traducción.
Ahora bien, ¿qué pasa si la declaración de clase contiene funciones virtuales puras? Un programador puede proporcionar una implementación para una función virtual pura, ¡pero no está obligado a hacerlo ! Por lo tanto, las funciones virtuales puras no pertenecen a la lista de métodos virtuales candidatos entre los que el compilador puede seleccionar el representativo.
Pero hay una excepción: ¡un destructor virtual puro!
Un destructor virtual puro es un caso especial:
- Una clase abstracta no tiene sentido si no vas a derivar otras clases de ella.
- El destructor de una subclase siempre llama al destructor de la clase base.
- El destructor de una clase derivada de una clase con un destructor virtual es automáticamente una función virtual.
- Todas las funciones virtuales de todas las clases, de las cuales el programa crea objetos, generalmente están vinculadas al ejecutable final (incluidas las funciones virtuales que se puede demostrar estáticamente que no se utilizan, aunque eso requeriría un análisis estático del programa completo).
- Por lo tanto, un destructor virtual puro debe tener una definición proporcionada por el usuario.
Por tanto, la advertencia de clang en el ejemplo de la pregunta no está conceptualmente justificada.
Sin embargo, desde el punto de vista práctico, la importancia de ese ejemplo es mínima, ya que rara vez se necesita un destructor virtual puro, si es que se necesita. No puedo imaginar un caso más o menos realista en el que un destructor virtual puro no vaya acompañado de otra función virtual pura. Pero en tal configuración la necesidad de la pureza del destructor (virtual) desaparece por completo, ya que la clase se vuelve abstracta debido a la presencia de otros métodos virtuales puros.