Erro ao usar a inicialização em classe de membro de dados não estáticos e construtor de classe aninhada

90

O código a seguir é bastante trivial e eu esperava que ele compilasse bem.

struct A
{
    struct B
    {
        int i = 0;
    };

    B b;

    A(const B& _b = B())
        : b(_b)
    {}
};

Testei este código com g ++ versão 4.7.2, 4.8.1, clang ++ 3.2 e 3.3. Além do fato de que g ++ 4.7.2 segfaults neste código ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), os outros compiladores testados fornecem mensagens de erro que não explicam muito.

g ++ 4.8.1:

test.cpp: In constructor constexpr A::B::B()’:
test.cpp:3:12: error: constructor required before non-static data member for A::B::i has been parsed
     struct B
            ^
test.cpp: At global scope:
test.cpp:11:23: note: synthesized method constexpr A::B::B()’ first required here 
     A(const B& _b = B())
                       ^

clang ++ 3.2 e 3.3:

test.cpp:11:21: error: defaulted default constructor of 'B' cannot be used by non-static data member initializer which appears before end of class definition
    A(const B& _b = B())
                    ^

Tornar este código compilável é possível e parece que não deve fazer diferença. Existem duas opções:

struct B
{
    int i = 0;
    B(){} // using B()=default; works only for clang++
};

ou

struct B
{
    int i;
    B() : i(0) {} // classic c++98 initialization
};

Este código está realmente incorreto ou os compiladores estão errados?

etam1024
fonte
3
Meu G ++ 4.7.3 diz internal compiler error: Segmentation faulta este código ...
Fred Foo
2
(erro C2864: 'A :: B :: i': apenas membros de dados integrais const estáticos podem ser inicializados dentro de uma classe) é o que diz o VC2010. Essa saída está de acordo com g ++. Clang também diz isso, embora faça muito menos sentido. Você não pode padronizar uma variável em uma estrutura fazendo a int i = 0menos que seja static const int i = 0.
Chris Cooper
@Borgleader: BTW, eu evitaria a tentação de pensar na expressão B()como uma chamada de função para um construtor. Você nunca "chama" diretamente um construtor. Pense nisso como uma sintaxe especial que cria um temporário B... e o construtor é invocado como apenas uma parte desse processo, nas profundezas do mecanismo que se segue.
Lightness Races in Orbit
2
Hmm, adicionar um construtor a Bparece fazer isso funcionar em gcc 4.7.
Shafik Yaghmour
7
Curiosamente, mover a definição do construtor de A para fora de A também parece fazê-lo funcionar (g ++ 4.7); que ressoa com "o construtor padrão não pode ser usado ... antes do final da definição da classe".
moonshadow de

Respostas:

84

Este código está realmente incorreto ou os compiladores estão errados?

Bem, nenhum. O padrão tem um defeito - ele diz que Aé considerado completo ao analisar o inicializador para B::ie aquele B::B()(que usa o inicializador para B::i) pode ser usado dentro da definição de A. Isso é claramente cíclico. Considere isto:

struct A {
  struct B {
    int i = (A(), 0);
  };
  A() noexcept(!noexcept(B()));
};

Isso tem uma contradição: B::B()é implicitamente noexceptiff A()não joga e A()não joga iff nãoB::B() é . Existem vários outros ciclos e contradições nesta área. noexcept

Isso é rastreado pelos problemas principais 1360 e 1397 . Observe, em particular, esta nota na edição principal 1397:

Talvez a melhor maneira de lidar com isso seja tornar malformado para um inicializador de membro de dados não estáticos usar um construtor padrão de sua classe.

Esse é um caso especial da regra que implementei no Clang para resolver esse problema. A regra de Clang é que um construtor padrão padronizado para uma classe não pode ser usado antes que os inicializadores de membros de dados não estáticos para essa classe sejam analisados. Portanto, o Clang emite um diagnóstico aqui:

    A(const B& _b = B())
                    ^

... porque o Clang analisa os argumentos padrão antes de analisar os inicializadores padrão, e este argumento padrão exigiria que Bos inicializadores padrão de já tivessem sido analisados ​​(a fim de definir implicitamente B::B()).

Richard Smith
fonte
Bom saber. Mas a mensagem de erro ainda é enganosa, uma vez que o construtor não é de fato "usado pelo inicializador de membro de dados não estáticos".
aschepler
Você sabia disso por causa de uma experiência anterior específica com esse problema ou apenas pela leitura cuidadosa do padrão (e da lista de defeitos)? Além disso, +1.
Cornstalks
1 para esta resposta detalhada. Então, qual seria a saída? Algum tipo de "análise de classe de 2 fases", em que a análise de membros de classes externas que dependem de classes internas é atrasada até que as classes internas tenham sido totalmente formadas?
TemplateRex
4
@aschepler Sim, o diagnóstico aqui não é muito bom. Eu preenchi llvm.org/PR16550 para isso.
Richard Smith
@Cornstalks Eu descobri esse problema ao implementar inicializadores para membros de dados não estáticos no Clang.
Richard Smith
0

Talvez este seja o problema:

§12.1 5. Um construtor padrão que é padronizado e não definido como excluído é implicitamente definido quando é odr- usado (3.2) para criar um objeto de seu tipo de classe (1.8) ou quando é explicitamente padronizado após sua primeira declaração

Portanto, o construtor padrão é gerado quando consultado pela primeira vez, mas a pesquisa falhará porque A não está completamente definido e B dentro de A, portanto, não será encontrado.

fscan
fonte
Não tenho certeza sobre esse "portanto". Claramente B bnão é um problema, e encontrar métodos explícitos / um construtor explicitamente declarado em Bnão é um problema. Portanto, seria bom ver alguma definição de por que a pesquisa deve proceder de forma diferente aqui, de modo que " Bdentro Anão seja encontrado" apenas neste caso, mas não nos outros, antes que possamos declarar o código ilegal por esse motivo.
moonshadow de
Eu encontrei palavras no padrão que afirmam que a definição de classe é considerada completa durante a inicialização em classe, incluindo dentro de classes aninhadas. Não me preocupei em registrar a referência, pois não parecia relevante.
Mark B
@moonshadow: a declaração diz que construtores implicitamente padrão são definidos quando odr- usado. é definido explicitamente após a primeira declaração. E B b não chama um construtor, o construtor de A chama o construtor de B
fscan
Se algo assim fosse o problema, o código ainda seria inválido se você remover o =0de i = 0;. Mas sem isso =0, o código é válido e você não encontrará um único compilador que reclame do uso B()dentro da definição de A.
aschepler