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?


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()).


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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.