O código abaixo reproduz o problema que tenho, MSVC 2022:
#include <iostream>
struct A {
static void message()
{
std::cout << "A::message()\n";
}
struct B {
B() {}
~B()
{
A::message();
}
};
static inline B b;
};
int main()
{
}
Com o código acima vejo, como esperado, a mensagem aparecendo. No entanto:
- Se eu remover o construtor
B()
, o destruidor~B()
parece ter sido eliminado porque a mensagem desaparece. - Se eu adicionar uma propriedade fictícia
int dummy;
à estruturaB
, a mensagem aparecerá novamente.
Perguntas para as quais realmente não consegui encontrar resposta:
- Parece-me que sem o construtor
B()
e sem propriedadesB
, o compilador simplesmente rotula a classeB
como vazia e simplesmente a ignora completamente. Isso está correto? - Está em algum lugar documentado que isso pode ocorrer? Dado o conteúdo do destruidor, acho que isso nunca deveria acontecer.
- Como posso garantir que sempre funcionará corretamente, não apenas para o compilador que estou usando no momento. Definir o construtor é
B()
suficiente ou devo também tornarB
não vazio com a propriedade fictíciaint dummy;
?
A diferença está em ter ou não
b
inicialização estática .Se uma variável de duração de armazenamento estático tiver inicialização estática, ela será inicializada antes de qualquer outra variável de duração de armazenamento estático sem inicialização estática (ou seja, inicialização dinâmica ).
Em particular, há outro objeto importante de duração de armazenamento estático definido em
<iostream>
, ou seja, uma instância destd::ios_base::Init
. A inicialização de um objeto desse tipo causa a inicialização dos fluxos IO padrão (por exemplo,std::cout
). Ele também é responsável por liberar todos esses fluxos quando a última instância deles for destruída.Portanto, para ver sua saída, você deve ter certeza de que o
std::ios_base::Init
objeto foi destruído após seu arquivob
. No entanto, a ordem de destruição é inversa à ordem de inicialização. Portanto, seb
tiver inicialização estática, mas ostd::ios_base::Init
objeto tiver inicialização dinâmica, então é o contrário. Seb
não tiver inicialização estática, então será ordenado corretamente, SOMENTE seb
a definição de existir em cada unidade de tradução que inclui (direta ou indiretamente)<iostream>
, após a primeira inclusão. Isso ocorre porque você usouinline
, o que faz com que a inicialização dinâmica sejab
apenas parcialmente ordenada .Geralmente, se a inicialização de uma variável estática de duração de armazenamento for uma expressão constante, então a variável terá inicialização estática. Esse é o caso aqui se você não declarar nenhum construtor.
Se a inicialização não for uma expressão constante, então ela geralmente tem inicialização dinâmica, exceto que, desde que não altere os valores resultantes das variáveis estáticas de duração de armazenamento após sua inicialização, a implementação é livre para escolher a inicialização estática para qualquer tal variável. Se você usar um não
constexpr
construtor ou deixar um membro não inicializado, você terá esta situação.Portanto, para obter o pedido correto com segurança, você precisa garantir que
b
não haja inicialização estática. Devido à margem de manobra dada à implementação, isso é um pouco complicado. Fazer algo noB
construtor do que obviamente só pode acontecer em tempo de execução deve ser suficiente (por exemplo, chamar algum IO), mas da forma como o padrão especifica isso atualmente, é difícil ter certeza absoluta porque as diferenças nos efeitos colaterais não são consideradas. Consulte a edição aberta 1294 do CWG .Uma alternativa melhor pode ser adicionar um
std::ios_base::Init
objeto como membro aB
, para que um desses objetos seja certamente mantido ativo enquantob
o destruidor de está em execução.Além disso, como você pode ver acima, a construção e destruição de variáveis de duração de armazenamento estático em C++ é bastante complicada. Se você puder, seria muito mais simples simplesmente declarar
b
como umastatic
variável não local emmain
. Geralmente não há nenhuma garantia de que astatic inline
variável seria construída antes da primeira não inicialização ou uso demain
qualquer maneira (isso é definido pela implementação).Observe que não verifiquei se essa é realmente a causa no caso do MSVC. A resposta é baseada em considerações teóricas do padrão e no que uma implementação poderia fazer.
Além disso, estou usando minha interpretação de [basic.start.term]/3 , que li como significando que a ordem de destruição das variáveis inicializadas estaticamente é ordenada com as variáveis inicializadas dinâmicas de acordo com sua ordem real de inicialização. Outra interpretação que pude ver no texto é que a ordem de destruição é como se todas as variáveis tivessem sido inicializadas dinamicamente de acordo com as regras de ordem de inicialização reversa para inicialização dinâmica. Infelizmente, o texto não me parece claro e pelo menos o GCC e o Clang parecem seguir a última interpretação. Nesse caso você não veria o seu problema.