Por que minha classe não é construtível por padrão?

28

Eu tenho essas aulas:

#include <type_traits>

template <typename T>
class A {
public:
    static_assert(std::is_default_constructible_v<T>);

};

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

    A<C> a_m;
};

int main() {
    A<B::C> a;
}

Ao compilar, a_mnão é construtível por padrão, mas aé.

Ao mudar Cpara:

struct C {
      int i;
   };

tudo está bem.

Testado com Clang 9.0.0.

Nicolas
fonte
3
GCC 8.3 - OK, GCC 9.1 / 9.2 - Falha.
Evg
2
Com C() {}isso também funciona.
Evg
3
Isso cheira buggy para mim. Nenhuma partida imediatamente óbvia no Bugzilla.
Lightness Races em órbita
2
Interessante: o static_assertin Afalha, mas se você construir um Tinterior Apor padrão (por exemplo, colocar um membro T t;lá), tudo funcionará bem. Uma inconsistência entre o que o traço de tipo está lhe dizendo e o que é realmente possível ...
sebrockm
2
@ Nicolas True, mas isso é devido a alguns casos extremos, nenhum dos quais se aplica aqui (em particular, como diz a mesma sentença na cppreference, const int x;é inválido sem um inicializador, devido exclusivamente ao constcomportamento de inicialização de tipos internos e alguns history)
Lightness Races in Orbit

Respostas:

9

Isso não é permitido pelo texto do padrão e por várias implementações importantes, conforme observado nos comentários, mas por razões completamente não relacionadas.

Primeiro, a razão "pelo livro": o ponto de instanciação de A<C>é, de acordo com o padrão, imediatamente antes da definição deB , e o ponto de instanciação de std::is_default_constructible<C>imediatamente antes disso:

Para uma especialização de modelo de classe, [...] se a especialização é implicitamente instanciada porque é referenciada em outra especialização de modelo, se o contexto do qual a especialização é referenciada depende de um parâmetro de modelo e se a especialização não é instanciada anteriormente para a instanciação do modelo anexo, o ponto de instanciação é imediatamente anterior ao ponto de instanciação do modelo anexo. Caso contrário, o ponto de instanciação para essa especialização precede imediatamente a declaração ou definição do escopo do espaço para nome que se refere à especialização.

Como Cé claramente incompleto nesse ponto, o comportamento da instanciação std::is_default_constructible<C>é indefinido. No entanto, consulte a edição principal 287 , que mudaria essa regra.


Na realidade, isso tem a ver com o NSDMI.

  • Os NSDMIs são estranhos porque são atrasados ​​na análise - ou, na linguagem padrão, são um "contexto de classe completa".
  • Portanto, isso = 0poderia, em princípio, se referir a coisas Bainda não declaradas, portanto a implementação não pode realmente tentar analisá-la até que termine B.
  • A conclusão de uma classe requer a declaração implícita de funções-membro especiais, em particular o construtor padrão, pois Cnão há um construtor declarado.
  • Partes dessa declaração (consexpr-ness, noexcept-ness) dependem das propriedades do NSDMI.
  • Portanto, se o compilador não puder analisar o NSDMI, não poderá concluir a classe.
  • Como resultado, no momento em que instancia A<C>, ele pensa que Cestá incompleto.

Toda essa área que lida com regiões com atraso de análise é lamentavelmente subespecificada, com as divergências na implementação. Pode demorar um pouco antes de ser limpo.

TC
fonte
0

Comportamento indefinido é:

Se uma instanciação de um modelo acima depender, direta ou indiretamente, de um tipo incompleto, e essa instanciação produzir um resultado diferente se esse tipo for hipoteticamente concluído, o comportamento será indefinido.

Esquecimento
fonte
7
Por que C está incompleto?
interjay 9/12/19
11
@interjay, Cestá completo, mas Bnão está. E B::Cdepende indiretamente B.
Evg
11
@Evg O texto "Depende direta ou indiretamente" aparece apenas em cppreference.com. O padrão apenas diz que o tipo T precisa ser completo.
interjay
2
@interjay Eu escrevi a maior parte dessa redação. O que estamos tentando dizer é que 1) se você instancia uma característica de uma maneira que possa provocar uma violação do ODR mais tarde quando algum tipo incompleto for concluído, isso é indefinido; e 2) é indefinido, mesmo que você realmente não cause uma violação de ODR em seu programa, para que as implementações de bibliotecas padrão possam optar por diagnosticar que no momento em que a característica é usada, se assim o desejar. Se Ctiver um modelo de construtor padrão com algum SFINAE estranho que possa alterar as respostas se Bfor concluído de maneira diferente, então a característica depende disso.
TC