it-swarm.it

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

Lo standard C++ (sezione 8.5) dice:

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

Perché? Non riesco a pensare ad alcun 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;
}
92
Karu

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

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

  • ciascun membro di dati non statici diretto non variante M di T ha un inizializzatore di membro predefinito o, se M è di tipo classe X (o matrice di esso), X è const-default-constructible,
  • se T è un'unione con almeno un membro di dati non statici, esattamente un membro di variante ha un inizializzatore di membro predefinito,
  • se T non è un'unione, per ogni membro di un sindacato anonimo con almeno un membro di dati non statici (se presente), esattamente un dato non statico membro ha un inizializzatore di membro predefinito e
  • ogni classe base potenzialmente costruita di T è costruttibile in base a valori predefiniti.

Se un programma richiede l'inizializzazione di default di un oggetto di il tipo const-qualificato T, T deve essere una classe const-default-constructible tipo o matrice dello stesso.

Questa formulazione significa essenzialmente che il codice ovvio funziona. Se si inizializzano tutte le basi e i membri, è possibile pronunciare A const a; indipendentemente da come o se si scrive qualsiasi costruttore.

struct A {
};
A const a;

gcc ha accettato questo dal 4.6.4. clang ha accettato questo dal 3.9.0. Visual Studio accetta anche questo (almeno nel 2017, non è sicuro se prima).

8
David Stone

La ragione è che se la classe non ha un costruttore definito dall'utente, allora può essere POD e la classe POD non è inizializzata di default. Quindi se dichiari un oggetto const di POD che non è inizializzato, a che cosa serve? Quindi penso che lo standard applichi 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 

Avviso nonPOD_B non definito costruttore definito dall'utente. Compilarlo Compilerà:

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


Beh, penso, hai frainteso il passaggio. Prima dice questo (§8.5/9):

Se non è specificato alcun inizializzatore per un oggetto, e l'oggetto è di tipo (eventualmente qualificato per il cv) non-POD class (o matrice dello stesso), l'oggetto deve essere inizializzato di default; [...]

Parla di classe non-POD possibilmente di tipo cv-qualificato. Cioè, l'oggetto non POD deve essere inizializzato di default se non è specificato alcun inizializzatore. E cos'è default-initialized? Per non-POD, la specifica dice (§8.5/5),

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

Parla semplicemente di costruttore predefinito di T, se il suo definito dall'utente o il compilatore generato è irrilevante.

Se sei chiaro su questo, allora comprendi cosa dice la specifica successiva ((§8.5/9),

[...]; se l'oggetto è di tipo const-qualified, 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 const-qualified POD, 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 compila bene , perché non POD, e può essere default-inizializzato.

65
Nawaz

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 anche (in modo ricorsivo) impedisce gli oggetti const (sub) unitizzati:

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 (consentendogli i tipi con un costruttore predefinito), penso che l'idea sia che un costruttore con un costruttore predefinito fornito dall'utente debba essere sempre in uno stato ragionevole dopo la costruzione. Si noti 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 sensibilmente inizializzato in un costruttore predefinito fornito dall'utente", ma è troppo tempo speso per cercare di proteggersi da Murphy. C++ tende a fidarsi del programmatore su determinati punti.

11
Luc Danton

Congratulazioni, hai inventato un caso in cui non è necessario alcun costruttore definito dall'utente per la dichiarazione const senza inizializzatore per avere un senso.

Ora puoi formulare una ragionevole riformulazione della regola che copre il tuo caso, ma continua a rendere i casi che dovrebbero essere illegali illegali? È inferiore a 5 o 6 paragrafi? È facile e ovvio come dovrebbe essere applicato in qualsiasi situazione?

Credo che sia una regola che permette che la dichiarazione che hai creato abbia un senso è davvero difficile, e assicurarti che la regola possa essere applicata in un modo che abbia senso per le persone che leggono il codice è ancora più difficile. Preferirei una regola piuttosto restrittiva che fosse la cosa giusta da fare nella maggior parte dei casi per una regola molto complessa e complessa che era difficile da comprendere e applicare.

La domanda è, c'è una ragione convincente che 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
Omnifarious

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

Considera due classi: A ha un costruttore user-dichiarato, B ha un costruttore user-provided:

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

A prima vista, potresti pensare che questi due costruttori si comportino allo stesso modo. Ma guarda come l'inizializzazione del valore si comporta diversamente, mentre solo l'inizializzazione di default si comporta allo stesso modo:

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

Ora vediamo cosa succede quando aggiungiamo const:

  • const A a; è l'inizializzazione di default: questo è mal formato a causa della regola citata nella domanda.
  • const B b; è l'inizializzazione predefinita: il membro int x non è inizializzato.
  • const A a{}; è l'inizializzazione del valore: il membro int x è zero-inizializzato.
  • const B b{}; è l'inizializzazione del valore: il membro int x non è inizializzato.

Uno scalare const non inizializzato (ad esempio il membro int x) sarebbe inutile: scrivere in esso è mal formato (perché è const) e leggere da esso è UB (perché contiene un valore indeterminato). Quindi questa regola ti impedisce di creare una cosa del genere, forzandoti a o aggiungere un inizializzatore o opt-in al comportamento pericoloso aggiungendo un costruttore fornito dall'utente.

Penso che sarebbe bello avere un attributo come [[uninitialized]] per dire al compilatore quando stai intenzionalmente non inizializzando un oggetto. Quindi non saremmo costretti a rendere la nostra classe non banale come predefinita per aggirare questo caso d'angolo.

0
Oktalist