Por que C ++ requer um construtor padrão fornecido pelo usuário para construir um objeto const por padrão?

99

O padrão C ++ (seção 8.5) diz:

Se um programa pede a inicialização padrão de um objeto de um tipo T qualificado por const, T deve obrigatoriamente ser um tipo de classe com um construtor padrão fornecido pelo usuário.

Por quê? Não consigo pensar em nenhuma razão pela qual um construtor fornecido pelo usuário seja necessário neste caso.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}
Karu
fonte
2
A linha não parece ser necessária em seu exemplo (consulte ideone.com/qqiXR ) porque você declarou, mas não definiu / inicializou a, mas gcc-4.3.4 aceita mesmo quando você o faz (consulte ideone.com/uHvFS )
Ray Toal
O exemplo acima declara e define a. Comeau produz um erro "variável const" a "requer um inicializador - a classe" A "não tem nenhum construtor padrão declarado explicitamente" se a linha estiver comentada.
Karu
4
Isso foi corrigido no C ++ 11, você pode escrever const A a{}:)
Howard Lovatt

Respostas:

10

Isso foi considerado um defeito (em todas as versões do padrão) e foi resolvido pelo Defeito 253 do Core Working Group (CWG) . A nova redação para os estados padrão em http://eel.is/c++draft/dcl.init#7

Um tipo de classe T é construtível por padrão constante se a inicialização padrão de T invocar um construtor de T fornecido pelo usuário (não herdado de uma classe base) ou se

  • cada membro de dados não estáticos não variantes diretos M de T tem um inicializador de membro padrão ou, se M for do tipo de classe X (ou matriz do mesmo), X é construtível por padrão constante,
  • se T é uma união com pelo menos um membro de dados não estáticos, exatamente um membro variante tem um inicializador de membro padrão,
  • se T não for uma união, para cada membro anônimo da união com pelo menos um membro de dados não estáticos (se houver), exatamente um membro de dados não estáticos tem um inicializador de membro padrão, e
  • cada classe base de T potencialmente construída é construtível por padrão constante.

Se um programa exige a inicialização padrão de um objeto de um tipo T qualificado por const, T deve ser um tipo de classe construtível por padrão const ou um array deste.

Esta formulação significa essencialmente que o código óbvio funciona. Se você inicializar todas as suas bases e membros, você pode dizer A const a;independentemente de como ou se soletrou quaisquer construtores.

struct A {
};
A const a;

O gcc aceitou isso desde 4.6.4. O clang aceita isso desde 3.9.0. O Visual Studio também aceita isso (pelo menos em 2017, não tenho certeza se antes).

David Stone
fonte
3
Mas isso ainda proíbe struct A { int n; A() = default; }; const A a;enquanto permite, struct B { int n; B() {} }; const B b;porque a nova redação ainda diz "fornecido pelo usuário" e não "declarado pelo usuário" e estou coçando minha cabeça por que o comitê escolheu excluir construtores padrão explicitamente inadimplentes deste DR, nos forçando a fazer nossas classes não são triviais se quisermos objetos const com membros não inicializados.
Oktalista de
1
Interessante, mas ainda há um caso extremo que encontrei. Com MyPODsendo um POD struct, static MyPOD x;- contando com zero de inicialização (que é o caminho certo?) Para definir a variável de membro (s) de forma apropriada - compila, mas static const MyPOD x;não o faz. Existe alguma chance de que isso seja consertado?
Joshua Green
66

O motivo é que, se a classe não tiver um construtor definido pelo usuário, pode ser POD, e a classe POD não é inicializada por padrão. Portanto, se você declarar um objeto const de POD que não foi inicializado, qual a utilidade disso? Portanto, acho que o Standard impõe essa regra para que o objeto possa realmente ser útil.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Mas se você fizer da aula um não-POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Observe a diferença entre POD e não-POD.

O construtor definido pelo usuário é uma maneira de tornar a classe não POD. Existem várias maneiras de fazer isso.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Observe que nonPOD_B não define o construtor definido pelo usuário. Compile-o. Ele irá compilar:

E comente a função virtual, então dá erro, conforme o esperado:


Bem, eu acho, você entendeu mal a passagem. Primeiro diz isto (§8.5 / 9):

Se nenhum inicializador for especificado para um objeto, e o objeto for do tipo de classe não-POD (possivelmente cv-qualificado) (ou array dele), o objeto deve obrigatoriamente ser inicializado por padrão; [...]

Ele fala sobre a classe não-POD, possivelmente do tipo cv qualificado . Ou seja, o objeto não-POD deve obrigatoriamente ser inicializado por padrão se não houver um inicializador especificado. E o que é inicializado por padrão ? Para não POD, a especificação diz (§8.5 / 5),

Inicializar por padrão um objeto do tipo T significa:
- se T for um tipo de classe não-POD (cláusula 9), o construtor padrão para T é chamado (e a inicialização é mal formada se T não tiver um construtor padrão acessível);

Ele simplesmente fala sobre o construtor padrão de T, seja seu definido pelo usuário ou gerado pelo compilador é irrelevante.

Se você está claro para isso, entenda o que a especificação diz a seguir ((§8.5 / 9),

[...]; se o objeto for do tipo qualificado const, o tipo de classe subjacente deve ter um construtor padrão declarado pelo usuário.

Portanto, este texto implica que o programa será malformado se o objeto for do tipo POD qualificado const e não houver um inicializador especificado (porque os POD não são inicializados por padrão):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

A propósito, ele compila bem , porque não é POD e pode ser inicializado por padrão .

Nawaz
fonte
1
Eu acredito que seu último exemplo é um erro de compilação - nonPOD_Bnão tem um construtor padrão fornecido pelo usuário, então a linha const nonPOD_B b2não é permitida.
Karu
1
Outra maneira de tornar a classe um não-POD é dando a ela um membro de dados que não seja um POD (por exemplo, minha estrutura Bna questão). Mas o construtor padrão fornecido pelo usuário ainda é necessário nesse caso.
Karu
"Se um programa exige a inicialização padrão de um objeto de um tipo T qualificado por const, T deve ser um tipo de classe com um construtor padrão fornecido pelo usuário."
Karu
@Karu: Eu li isso. Parece que há outras passagens na especificação, o que permite que o constobjeto não-POD seja inicializado chamando o construtor padrão gerado pelo compilador.
Nawaz,
2
Os links da sua ideone parecem estar quebrados e seria ótimo se esta resposta pudesse ser atualizada para C ++ 11/14 porque o §8.5 não menciona POD.
Oktalista de
12

Pura especulação da minha parte, mas considere que outros tipos também têm uma restrição semelhante:

int main()
{
    const int i; // invalid
}

Portanto, essa regra não é apenas consistente, mas também (recursivamente) evita const(sub) objetos unitializados :

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Quanto ao outro lado da questão (permitindo-o para tipos com um construtor padrão), acho que a ideia é que um tipo com um construtor padrão fornecido pelo usuário deve sempre estar em algum estado lógico após a construção. Observe que as regras permitem o seguinte:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Então, talvez pudéssemos formular uma regra como 'pelo menos um membro deve ser inicializado sensatamente em um construtor padrão fornecido pelo usuário', mas isso é muito tempo gasto tentando proteger contra Murphy. C ++ tende a confiar no programador em certos pontos.

Luc Danton
fonte
Mas ao adicionar A(){}, o erro vai embora, por isso não impede nada. A regra não funciona recursivamente - X(){}nunca é necessária para esse exemplo.
Karu
2
Bem, pelo menos forçando o programador a adicionar um construtor, ele é forçado a pensar um minuto no problema e talvez chegar a um não trivial
arne
@Karu Eu respondi apenas metade da pergunta - consertei isso :)
Luc Danton
4
@arne: O único problema é que é o programador errado. A pessoa que está tentando instanciar a classe pode pensar muito sobre o assunto, mas pode não ser capaz de modificar a classe. O autor da classe pensou sobre os membros, viu que todos eles foram inicializados de forma sensata pelo construtor padrão implícito, então nunca adicionou um.
Karu,
3
O que eu tirei dessa parte do padrão é "sempre declare sempre um construtor padrão para tipos não-POD, no caso de alguém querer fazer uma instância const um dia". Isso parece um pouco exagerado.
Karu
3

Eu estava assistindo a palestra de Timur Doumler no Meeting C ++ 2018 e finalmente percebi por que o padrão requer um construtor fornecido pelo usuário aqui, não apenas um declarado pelo usuário. Tem a ver com as regras de inicialização de valor.

Considere duas classes: Atem um construtor declarado pelo usuário , Btem um construtor fornecido pelo usuário :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

À primeira vista, você pode pensar que esses dois construtores se comportarão da mesma forma. Mas veja como a inicialização de valor se comporta de maneira diferente, enquanto apenas a inicialização padrão se comporta da mesma forma:

  • A a;é a inicialização padrão: o membro int xnão foi inicializado.
  • B b;é a inicialização padrão: o membro int xnão foi inicializado.
  • A a{};é a inicialização do valor: o membro int xé inicializado com zero .
  • B b{};é a inicialização do valor: o membro int xnão foi inicializado.

Agora veja o que acontece quando adicionamos const:

  • const A a;é a inicialização padrão: está mal formada devido à regra citada na pergunta.
  • const B b;é a inicialização padrão: o membro int xnão foi inicializado.
  • const A a{};é a inicialização do valor: o membro int xé inicializado com zero .
  • const B b{};é a inicialização do valor: o membro int xnão foi inicializado.

Um constescalar não inicializado (por exemplo, o int xmembro) seria inútil: escrever nele é malformado (porque é const) e ler a partir dele é UB (porque contém um valor indeterminado). Então Esta regra impede de criar uma coisa dessas, forçando você a quer adicionar um initialiser ou opt-in para o comportamento perigoso, adicionando um construtor fornecido pelo usuário.

Acho que seria bom ter um atributo como [[uninitialized]]avisar ao compilador quando você não está inicializando intencionalmente um objeto. Então, não seríamos forçados a tornar nossa classe não trivialmente construtível por padrão para contornar esse caso. Este atributo já foi proposto , mas assim como todos os outros atributos padrão, não impõe nenhum comportamento normativo, sendo apenas uma dica para o compilador.

Oktalist
fonte
1

Parabéns, você inventou um caso em que não precisa haver nenhum construtor definido pelo usuário para que a constdeclaração sem inicializador faça sentido.

Agora, você pode propor uma reformulação razoável da regra que cubra seu caso, mas ainda torne os casos que deveriam ser ilegais, ilegais? Tem menos de 5 ou 6 parágrafos? É fácil e óbvio como deve ser aplicado em qualquer situação?

Acredito que criar uma regra que permita que a declaração que você criou faça sentido é muito difícil, e certificar-se de que a regra pode ser aplicada de uma forma que faça sentido para as pessoas ao ler o código é ainda mais difícil. Eu preferiria uma regra um tanto restritiva que fosse a coisa certa a fazer na maioria dos casos a uma regra muito sutil e complexa que fosse difícil de entender e aplicar.

A questão é: há uma razão convincente para que a regra seja mais complexa? Existe algum código que de outra forma seria muito difícil de escrever ou entender que pode ser escrito de forma muito mais simples se a regra for mais complexa?

Omniforme
fonte
1
Aqui está minha sugestão de redação: "Se um programa requer a inicialização padrão de um objeto de um tipo T qualificado por const, T deve ser um tipo de classe não POD.". Isso seria const POD x;ilegal, assim como const int x;é ilegal (o que faz sentido, porque isso é inútil para um POD), mas seria const NonPOD x;legal (o que faz sentido, porque poderia ter subobjetos contendo construtores / destruidores úteis, ou ter um construtor / destruidor útil) .
Karu
@Karu - Esse texto pode funcionar. Estou acostumado com os padrões RFC e, portanto, sinto que 'T deve ser' deveria ser lido como 'T deve ser'. Mas sim, isso poderia funcionar.
Onifário
@Karu - E quanto a struct NonPod {int i; void virtual f () {}}? Não faz sentido criar const NonPod x; legal.
gruzovator
1
@gruzovator Faria mais sentido se você tivesse um construtor padrão vazio declarado pelo usuário? Minha sugestão apenas tenta remover um requisito inútil do padrão; com ou sem ele, ainda existem infinitas maneiras de escrever código que não faz sentido.
Karu
1
@Karu eu concordo com você. Por causa dessa regra no padrão, há muitas classes que precisam ter um construtor vazio definido pelo usuário . Eu gosto do comportamento do gcc. Ele permite, por exemplo, struct NonPod { std::string s; }; const NonPod x;e dá um erro quando NonPod éstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator