Um compilador pode colocar a implementação de um destruidor virtual declarado implicitamente em uma única unidade de tradução separada?

8

O código a seguir compila e vincula com Visual Studio(2017 e 2019 com /permissive-), mas não compila com gccou 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>();
}

Meu entendimento é que, em main.cpp, Foo::Bardeve ser um tipo completo, porque sua exclusão é tentada ~Foo(), o que é implicitamente declarado e, portanto, implicitamente definido em todas as unidades de tradução que o acessam.

No entanto, Visual Studionão concorda e aceita esse código. Além disso, descobri que as seguintes alterações Visual Studiorejeitam o código:

  • Tornando (1)não virtual
  • Definindo (2)inline - Foo() = default;ou seja,Foo(){};
  • Removendo (3)

Parece-me Visual Studioque não define um destruidor implícito em todos os lugares em que é usado nas seguintes condições:

  • O destruidor implícito é virtual
  • A classe possui um construtor definido em uma unidade de tradução diferente

Em vez disso, parece definir apenas o destruidor na unidade de conversão que também contém a definição para o construtor na segunda condição.

Então agora eu estou me perguntando:

  • Isso é permitido?
  • É especificado em algum lugar, ou pelo menos conhecido, que Visual Studiofaz isso?

Atualização: arquivei um relatório de bug https://developercommunity.visualstudio.com/content/problem/790224/implictly-declared-virtual-destructor-does-not-app.html . Vamos ver o que os especialistas acham disso.

Marca
fonte
1
O que acontece se você criar o código com o Visual Studio com a opção / permissive- ?
Jesper Juhl
1
Mesmo resultado. Vou colocar isso em questão.
Mark
1
As alterações 2 e 3 são claras, você precisa de um tipo completo quando o deleter (padrão) é chamado (no destruidor de unique_ptr, o que novamente acontece no construtor do Foo, portanto, quando o último está embutido, o tipo precisa estar completo já no cabeçalho). A mudança 1 me surpreende, porém, não há explicação para.
Aconcagua
Adicione isso ao Foo: struct BarDeleter { void operator()(Bar*) const noexcept; };e altere o unique_ptr para std::unique_ptr<Bar, BarDeleter> bar_;. Em seguida, na unidade de tradução da implementação, adicionevoid Foo::BarDeleter::operator()(Foo::Bar* p) const noexcept { try { delete p; } catch(...) {/*discard*/}}
Eljay

Respostas:

2

Eu acredito que isso seja um bug no MSVC. Quanto ao std::default_delete::operator(), o Padrão diz que [unique.ptr.dltr.dflt / 4] :

Comentários: Se T é um tipo incompleto, o programa está incorreto .

Como não há cláusula "não é necessário diagnóstico" , é necessário um compilador C ++ em conformidade para emitir um diagnóstico [intro.compliance / 2.2] :

Se um programa contiver uma violação de qualquer regra diagnosticável ou ..., uma implementação em conformidade emitirá pelo menos uma mensagem de diagnóstico .

junto com [introdução / conformidade / 1] :

O conjunto de regras diagnosticáveis consiste em todas as regras sintáticas e semânticas deste documento, exceto aquelas que contêm uma notação explícita de que "nenhum diagnóstico é necessário" ou que são descritas como resultando em "comportamento indefinido".


O GCC usa static_assertpara diagnosticar a integridade do tipo. O MSVC aparentemente não realiza essa verificação. Se ele passa silenciosamente um parâmetro de std::default_delete::operator()para delete, isso causa um comportamento indefinido . O que pode corresponder à sua observação. Pode funcionar, mas até que seja garantida pela documentação (como uma extensão C ++ não padrão), eu não a usaria.

Daniel Langr
fonte
Este é o meu raciocínio também, até agora.
Mark
1
@DanielLangr OHHHH [@ $% * & +!] , Totalmente esquecido, é o construtor padrão fornecido, não o destruidor !!! Ter um destruidor virtual na classe base já me afastou ... Desculpe . Você está totalmente certo então, é claro. Querendo saber agora se proporcionando ctor em vez de dtor foi deliberada ou por acidente ...
Aconcagua
1
@Aconcagua, é deliberado. O problema é que o msvc define o destruidor implícito em uma unidade de tradução onde não é usado e não define em uma unidade de tradução em que é usado.
Mark
1
@DanielLangr Obrigado pelo link ... Mas faz sentido para o construtor padrão, bem, basta considerar uma classe com membros complexas:class Demo { std::vector<int> data; };
Aconcagua
1
@Aconcagua acho que finalmente entendi. O problema não está no construtor padrão de std::unique_ptr<Bar>. O problema está no construtor padrão de Foo. Se houver uma exceção, será Foo::Foo()necessário destruir o subobjeto já construído bar_(reversão).
Daniel Langr 22/10/19