¿Puede un compilador colocar la implementación de un destructor virtual declarado implícitamente en una sola unidad de traducción separada?

8

El siguiente código compila y enlaza con Visual Studio(tanto 2017 como 2019 con /permissive-), pero no compila con ninguno gcco clang.

foo.h

#include <memory>

struct Base {
    virtual ~Base() = default; // (1)
};

struct Foo : public Base {
    Foo();                     // (2)
    struct Bar;
    std::unique_ptr<Bar> bar_;
};

foo.cpp

#include "foo.h"

struct Foo::Bar {};            // (3)
Foo::Foo() = default;

main.cpp

#include "foo.h"

int main() {
    auto foo = std::make_unique<Foo>();
}

Tengo entendido que, en main.cpp, Foo::Bardebe ser un tipo completo, porque se intenta eliminarlo ~Foo(), lo que se declara implícitamente y, por lo tanto, se define implícitamente en cada unidad de traducción que accede a él.

Sin embargo, Visual Studiono está de acuerdo y acepta este código. Además, descubrí que los siguientes cambios hacen que Visual Studiorechace el código:

  • Hacer (1)no virtual
  • Definición en (2)línea - es decir Foo() = default;oFoo(){};
  • Quitando (3)

Me parece Visual Studioque no define un destructor implícito en todas partes donde se usa bajo las siguientes condiciones:

  • El destructor implícito es virtual
  • La clase tiene un constructor que se define en una unidad de traducción diferente.

En cambio, parece solo definir el destructor en la unidad de traducción que también contiene la definición del constructor en la segunda condición.

Entonces ahora me pregunto:

  • ¿Esto está permitido?
  • ¿Se especifica en alguna parte, o al menos se sabe, que Visual Studiohace esto?

Actualización: he presentado un informe de error https://developercommunity.visualstudio.com/content/problem/790224/implictly-declared-virtual-destructor-does-not-app.html . Veamos qué piensan los expertos de esto.

marca
fuente
1
¿Qué sucede si compila el código con Visual Studio con el modificador / permissive- ?
Jesper Juhl
1
Mismo resultado. Pondré eso en la pregunta.
Mark
1
Los cambios 2 y 3 son claros, necesita un tipo completo cuando se llama al eliminador (predeterminado) (en el destructor de unique_ptr, que nuevamente ocurre en el constructor de Foo, por lo que cuando este último está en línea, el tipo debe estar completo ya en el encabezado). El cambio 1 me sorprende, sin embargo, no hay explicación.
Aconcagua
Agregue esto a Foo: struct BarDeleter { void operator()(Bar*) const noexcept; };y cambie el unique_ptr a std::unique_ptr<Bar, BarDeleter> bar_;. Luego, en la unidad de traducción de implementación, agreguevoid Foo::BarDeleter::operator()(Foo::Bar* p) const noexcept { try { delete p; } catch(...) {/*discard*/}}
Eljay

Respuestas:

2

Creo que esto es un error en MSVC. En cuanto a std::default_delete::operator(), el Estándar dice que [unique.ptr.dltr.dflt / 4] :

Observaciones: si T es un tipo incompleto, el programa está mal formado .

Como no hay una cláusula de "no se requiere diagnóstico" , se requiere un compilador C ++ conforme para emitir un diagnóstico [intro.compliance / 2.2] :

Si un programa contiene una violación de una regla diagnosticable o ..., una implementación conforme emitirá al menos un mensaje de diagnóstico .

junto con [introducción / cumplimiento / 1] :

El conjunto de reglas diagnosticables consta de todas las reglas sintácticas y semánticas en este documento, excepto las reglas que contienen una notación explícita de que "no se requiere diagnóstico" o que se describen como resultado de "comportamiento indefinido".


GCC utiliza static_assertpara diagnosticar la integridad del tipo. MSVC aparentemente no realiza tal verificación. Si pasa silenciosamente un parámetro de std::default_delete::operator()a delete, esto provoca un comportamiento indefinido . Lo que podría corresponder con su observación. Puede funcionar, pero hasta que no esté garantizado por la documentación (como una extensión de C ++ no estándar), no lo usaría.

Daniel Langr
fuente
Este es mi razonamiento también, hasta ahora.
Mark
1
@DanielLangr OHHHH [@ $% * & +!] , Totalmente ignorado, ¡es el constructor predeterminado que se proporciona, no el destructor ! Tener un destructor virtual en la clase base ya me ha alejado ... Lo siento . Tienes toda la razón entonces, por supuesto. Ahora me pregunto si proporcionar ctor en lugar de dtor fue deliberado o por accidente ...
Aconcagua
1
@Aconcagua, es deliberado. El problema es que msvc define el destructor implícito en una unidad de traducción donde no se usa, y no lo define en una unidad de traducción donde se usa.
Mark
1
@DanielLangr Gracias por el enlace ... Pero también tiene sentido para el constructor predeterminado, solo considere una clase con miembros complejos:class Demo { std::vector<int> data; };
Aconcagua
1
@Aconcagua Creo que finalmente lo entiendo. El problema no es con el constructor predeterminado de std::unique_ptr<Bar>. El problema es con el constructor predeterminado de Foo. Si hay una excepción, Foo::Foo()debe destruir el subobjeto ya construido bar_(reversión).
Daniel Langr