Perché C ++ richiede un costruttore predefinito fornito dall'utente per costruire in modo predefinito un oggetto const?


99

Lo standard C ++ (sezione 8.5) dice:

Se un programma richiede l'inizializzazione predefinita di un oggetto di un tipo T qualificato da const, T deve essere un tipo di classe con un costruttore predefinito fornito dall'utente.

Perché? Non riesco a pensare a nessun motivo per cui in questo caso sia richiesto un costruttore fornito dall'utente.

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;
}

2
La linea non sembrerebbe essere richiesta nel tuo esempio (vedi ideone.com/qqiXR ) perché hai dichiarato ma non hai definito / inizializzato a, ma gcc-4.3.4 lo accetta anche quando lo fai (vedi ideone.com/uHvFS )
Ray Toal

L'esempio sopra sia dichiara che definisce a. Comeau produce un errore "variabile const" a "richiede un inizializzatore - la classe" A "non ha un costruttore predefinito dichiarato esplicitamente" se la riga è commentata.
Karu


4
Questo problema è stato risolto in C ++ 11, puoi scrivere const A a{}:)
Howard Lovatt

Risposte:


10

Questo è stato considerato un difetto (contro tutte le versioni dello standard) ed è stato risolto dal Difetto 253 del Core Working Group (CWG) . La nuova formulazione per gli stati standard in http://eel.is/c++draft/dcl.init#7

Un tipo di classe T è const-default-costruibile se l'inizializzazione predefinita di T richiama un costruttore di T fornito dall'utente (non ereditato da una classe base) o se

  • ogni membro di dati non statico non variante diretto M di T ha un inizializzatore di membro predefinito o, se M è di tipo di classe X (o array di esso), X è const-default-costruibile,
  • se T è un'unione con almeno un membro dati non statico, esattamente un membro variante ha un inizializzatore membro predefinito,
  • se T non è un'unione, per ogni membro di unione anonimo con almeno un membro di dati non statici (se presente), esattamente un membro di dati non statici ha un inizializzatore di membro predefinito e
  • ogni classe base di T potenzialmente costruita è const-default-costruibile.

Se un programma richiede l'inizializzazione predefinita di un oggetto di un tipo T qualificato da const, T deve essere un tipo di classe costruibile con impostazione predefinita o un array dello stesso.

Questa formulazione significa essenzialmente che il codice ovvio funziona. Se inizializzi tutte le tue basi e membri, puoi dire A const a;indipendentemente da come o se scrivi i costruttori.

struct A {
};
A const a;

gcc lo ha accettato dalla 4.6.4. clang lo ha accettato dalla 3.9.0. Anche Visual Studio lo accetta (almeno nel 2017, non so se prima).


3
Ma questo ancora vieta struct A { int n; A() = default; }; const A a;pur consentendo struct B { int n; B() {} }; const B b;perché la nuova formulazione dice ancora "fornito dall'utente" non "dichiarato dall'utente" e non mi resta che grattarmi la testa perché il comitato ha scelto di escludere i costruttori predefiniti esplicitamente predefiniti da questo DR, costringendoci a fare le nostre classi non sono banali se vogliamo oggetti const con membri non inizializzati.
Oktalist

1
Interessante, ma c'è ancora un caso limite in cui mi sono imbattuto. Con MyPODessendo un POD struct, static MyPOD x;- basandosi su zero inizializzazione (che è quella giusta?) Per impostare la variabile membro (s) opportunamente - compilazioni, ma static const MyPOD x;non lo fa. C'è qualche possibilità che che otterrà fisso?
Joshua Green

66

Il motivo è che se la classe non ha un costruttore definito dall'utente, allora può essere POD e la classe POD non è inizializzata per impostazione predefinita. Quindi, se dichiari un oggetto const di POD che non è inizializzato, a cosa serve? Quindi penso che lo Standard imponga questa regola in modo che l'oggetto possa effettivamente essere utile.

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!

Ma se rendi la classe un non-POD:

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

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

Notare la differenza tra POD e non-POD.

Il costruttore definito dall'utente è un modo per rendere la classe non POD. Ci sono diversi modi per farlo.

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

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

Si noti che nonPOD_B non definisce un costruttore definito dall'utente. Compilalo. Compilerà:

E commenta la funzione virtuale, quindi dà l'errore, come previsto:


Beh, penso che tu abbia frainteso il passaggio. Per prima cosa dice questo (§8.5 / 9):

Se per un oggetto non è specificato alcun inizializzatore e l'oggetto è di tipo (possibilmente qualificato da cv) non-POD di tipo (o array), l'oggetto deve essere inizializzato per default; [...]

Si parla di classi non POD possibilmente qualificate per cv . Cioè, l'oggetto non POD deve essere inizializzato per impostazione predefinita se non è specificato alcun inizializzatore. E cos'è l' inizializzazione predefinita ? Per i non POD, le specifiche dicono (§8.5 / 5),

Inizializzare di default un oggetto di tipo T significa:
- se T è un tipo di classe non POD (clausola 9), viene chiamato il costruttore di default per T (e l'inizializzazione è mal formata se T non ha un costruttore di default accessibile);

Parla semplicemente del costruttore predefinito di T, indipendentemente dal fatto che il suo definito dall'utente o generato dal compilatore sia irrilevante.

Se sei chiaro fino a questo punto, allora capisci cosa dice la specifica successiva ((§8.5 / 9),

[...]; se l'oggetto è di tipo qualificato const, il tipo di classe sottostante deve avere un costruttore predefinito dichiarato dall'utente.

Quindi questo testo implica che il programma sarà mal formato se l'oggetto è di tipo POD qualificato const e non è specificato alcun inizializzatore (perché i POD non sono inizializzati di default):

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

A proposito, questo si compila bene , perché non è POD e può essere inizializzato di default .


1
Credo che il tuo ultimo esempio sia un errore di compilazione: nonPOD_Bnon ha un costruttore predefinito fornito dall'utente, quindi la riga const nonPOD_B b2non è consentita.
Karu

1
Un altro modo per rendere la classe un non-POD è assegnarle un membro dati che non è un POD (es. La mia struttura Bnella domanda). Ma il costruttore predefinito fornito dall'utente è ancora necessario in questo caso.
Karu

"Se un programma richiede l'inizializzazione predefinita di un oggetto di un tipo T qualificato da const, T deve essere un tipo di classe con un costruttore predefinito fornito dall'utente."
Karu

@Karu: l'ho letto. Sembra che ci siano altri passaggi nelle specifiche, che consentono constdi inizializzare oggetti non POD chiamando il costruttore predefinito generato dal compilatore.
Nawaz

2
I tuoi collegamenti ideone sembrano essere interrotti, e sarebbe fantastico se questa risposta potesse essere aggiornata a C ++ 11/14 perché §8.5 non menziona affatto POD.
Oktalist

12

Pura speculazione da parte mia, ma considera che anche altri tipi hanno una restrizione simile:

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

Quindi non solo questa regola è coerente, ma impedisce anche (ricorsivamente) di oggetti const(sub) unitializzati :

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

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

Per quanto riguarda l'altro lato della domanda (consentendolo per i tipi con un costruttore predefinito), penso che l'idea sia che un tipo con un costruttore predefinito fornito dall'utente dovrebbe essere sempre in uno stato ragionevole dopo la costruzione. Tieni presente che le regole così come sono consentono quanto segue:

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

Quindi forse potremmo formulare una regola come "almeno un membro deve essere inizializzato in modo sensato in un costruttore predefinito fornito dall'utente", ma è troppo tempo speso cercando di proteggersi da Murphy. Il C ++ tende a fidarsi del programmatore su alcuni punti.


Ma aggiungendo A(){}, l'errore scompare, quindi non impedisce nulla. La regola non funziona in modo ricorsivo - X(){}non è mai necessaria per quell'esempio.
Karu

2
Ebbene, almeno costringendo il programmatore ad aggiungere un costruttore, è costretto a pensare un minuto al problema e magari a inventarne uno non banale
arne

@Karu Ho risposto solo a metà della domanda - risolto :)
Luc Danton

4
@arne: L'unico problema è che è il programmatore sbagliato. La persona che cerca di creare un'istanza della classe potrebbe dare tutto il pensiero che vuole all'argomento, ma potrebbe non essere in grado di modificare la classe. L'autore della classe ha pensato ai membri, ha visto che erano tutti inizializzati in modo sensato dal costruttore predefinito implicito, quindi non ne ha mai aggiunto uno.
Karu

3
Quello che ho preso da questa parte dello standard è "dichiara sempre sempre un costruttore predefinito per i tipi non POD, nel caso in cui qualcuno voglia creare un'istanza const un giorno". Sembra un po 'eccessivo.
Karu

3

Stavo guardando il discorso di Timur Doumler al Meeting C ++ 2018 e ho finalmente capito perché lo standard richiede un costruttore fornito dall'utente qui, non semplicemente uno dichiarato dall'utente. Ha a che fare con le regole per l'inizializzazione del valore.

Considera due classi: Aha un costruttore dichiarato dall'utente , Bha un costruttore fornito dall'utente :

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

A prima vista, potresti pensare che questi due costruttori si comporteranno allo stesso modo. Ma guarda come l'inizializzazione del valore si comporta in modo diverso, mentre solo l'inizializzazione predefinita si comporta allo stesso modo:

  • A a;è l'inizializzazione predefinita: il membro int xnon è inizializzato.
  • B b;è l'inizializzazione predefinita: il membro int xnon è inizializzato.
  • A a{};è l'inizializzazione del valore: l'elemento int xè inizializzato con zero .
  • B b{};è l'inizializzazione del valore: il membro int xnon è inizializzato.

Ora guarda cosa succede quando aggiungiamo const:

  • const A a;è l'inizializzazione predefinita: è mal formata a causa della regola citata nella domanda.
  • const B b;è l'inizializzazione predefinita: il membro int xnon è inizializzato.
  • const A a{};è l'inizializzazione del valore: l'elemento int xè inizializzato con zero .
  • const B b{};è l'inizializzazione del valore: il membro int xnon è inizializzato.

Uno constscalare non inizializzato (ad esempio il int xmembro) sarebbe inutile: scrivere su di esso è mal formato (perché è const) e leggere da esso è UB (perché contiene un valore indeterminato). Così questa regola impedisce la creazione di una cosa del genere, da ti costringe a uno aggiungere un initialiser o opt-in al comportamento pericoloso con l'aggiunta di un costruttore fornito dall'utente.

Penso che sarebbe bello avere un attributo come [[uninitialized]]dire al compilatore quando intenzionalmente non stai inizializzando un oggetto. Quindi non saremmo costretti a rendere la nostra classe non banalmente costruibile di default per aggirare questo caso d'angolo. Questo attributo è stato effettivamente proposto , ma proprio come tutti gli altri attributi standard, non impone alcun comportamento normativo, essendo solo un suggerimento per il compilatore.


1

Congratulazioni, hai inventato un caso in cui non è necessario alcun costruttore definito dall'utente affinché la constdichiarazione senza inizializzatore abbia un senso.

Ora puoi elaborare una ragionevole riformulazione della regola che copre il tuo caso ma rende comunque illegali i casi che dovrebbero essere illegali? È inferiore a 5 o 6 paragrafi? È facile e ovvio come dovrebbe essere applicato in ogni situazione?

Suppongo che inventare una regola che permetta alla dichiarazione che hai creato di avere un senso sia davvero difficile e assicurarsi che la regola possa essere applicata in un modo che abbia senso per le persone quando leggono il codice è ancora più difficile. Preferirei una regola un po 'restrittiva che era la cosa giusta da fare nella maggior parte dei casi a una regola molto sfumata e complessa che era difficile da capire e applicare.

La domanda è: c'è una ragione convincente per cui la regola dovrebbe essere più complessa? Esiste un codice che altrimenti sarebbe molto difficile da scrivere o capire che può essere scritto molto più semplicemente se la regola è più complessa?


1
Ecco la mia formulazione suggerita: "Se un programma richiede l'inizializzazione predefinita di un oggetto di un tipo T qualificato con const, T deve essere un tipo di classe non POD.". Ciò renderebbe const POD x;illegale proprio come const int x;è illegale (il che ha senso, perché questo è inutile per un POD), ma sarebbe const NonPOD x;legale (il che ha senso, perché potrebbe avere sottooggetti contenenti utili costruttori / distruttori, o avere un utile costruttore / distruttore stesso) .
Karu

@Karu - Quella formulazione potrebbe funzionare. Sono abituato agli standard RFC e quindi ritengo che "T deve essere" dovrebbe leggere "T deve essere". Ma sì, potrebbe funzionare.
Omnifarious

@Karu - Che ne dici di struct NonPod {int i; vuoto virtuale f () {}}? Non ha senso creare const NonPod x; legale.
gruzovator

1
@gruzovator Avrebbe più senso se avessi un costruttore predefinito vuoto dichiarato dall'utente? Il mio suggerimento tenta solo di rimuovere un requisito inutile dello standard; con o senza ci sono ancora infiniti modi per scrivere codice che non ha senso.
Karu

1
@Karu sono d'accordo con te. A causa di questa regola nello standard ci sono molte classi che devono avere un costruttore vuoto definito dall'utente . Mi piace il comportamento di gcc. Permette ad esempio struct NonPod { std::string s; }; const NonPod x;E dà un errore quando NonPod èstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.