Costruttore predefinito eliminato. Gli oggetti possono ancora essere creati … a volte

La vista ingenua, ottimistica e oh .. così errata della syntax di inizializzazione uniforms del c ++ 11

Ho pensato che dal momento che gli oggetti di tipo definito dall’utente C ++ 11 dovrebbero essere costruiti con la nuova syntax {...} invece della vecchia (...) syntax (eccetto per il costruttore sovraccaricato per std::initializer_list e parametri simili (es. std::vector : size ctor vs 1 elem init_list ctor)).

I vantaggi sono: nessuna conversione implicita ristretta, nessun problema con l’analisi più irritante, consistenza (?). Non ho visto nessun problema perché pensavo che fossero uguali (tranne l’esempio dato).

Ma loro non lo sono.

Una storia di pura pazzia

Il {} chiama il costruttore predefinito.

… Tranne quando:

  • il costruttore predefinito è cancellato e
  • non ci sono altri costruttori definiti.

Quindi sembra che il valore piuttosto inizializzi l’object? … Anche se l’object ha eliminato il costruttore predefinito, il {} può creare un object. Questo non ha battuto l’intero scopo di un costruttore cancellato?

… Tranne quando:

  • l’object ha un costruttore predefinito eliminato e
  • altri costruttori definiti.

Quindi fallisce con la call to deleted constructor .

… Tranne quando:

  • l’object ha un costruttore cancellato e
  • nessun altro costruttore definito e
  • almeno un membro di dati non statici.

Quindi fallisce con gli inizializzatori di campo mancanti.

Ma allora puoi usare {value} per build l’object.

Ok forse è uguale alla prima eccezione (valore init l’object)

… Tranne quando:

  • la class ha un costruttore cancellato
  • e almeno un membro di dati predefinito di class inizializzato.

Quindi né {}{value} possono creare un object.

Sono sicuro che ne ho mancati alcuni. L’ironia è che si chiama syntax di inizializzazione uniforms . Ripeto : syntax di inizializzazione UNIFORM .

Cos’è questa follia?

Scenario A

Costruttore predefinito eliminato:

 struct foo { foo() = delete; }; // All bellow OK (no errors, no warnings) foo f = foo{}; foo f = {}; foo f{}; // will use only this from now on. 

Scenario B

Costruttore predefinito eliminato, altri costruttori cancellati

 struct foo { foo() = delete; foo(int) = delete; }; foo f{}; // OK 

Scenario C

Costruttore predefinito eliminato, altri costruttori definiti

 struct foo { foo() = delete; foo(int) {}; }; foo f{}; // error call to deleted constructor 

Scenario D

Costruttore predefinito eliminato, nessun altro costruttore definito, membro dati

 struct foo { int a; foo() = delete; }; foo f{}; // error use of deleted function foo::foo() foo f{3}; // OK 

Scenario E

Costruttore predefinito eliminato, costruttore T eliminato, membro dati T

 struct foo { int a; foo() = delete; foo(int) = delete; }; foo f{}; // ERROR: missing initializer foo f{3}; // OK 

Scenario F

Costruttore predefinito eliminato, inizializzatori dei membri dei dati in class

 struct foo { int a = 3; foo() = delete; }; /* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()` /* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)` 

Osservando le cose in questo modo è facile dire che c’è un caos completo e totale nel modo in cui un object viene inizializzato.

La grande differenza deriva dal tipo di foo : se è un tipo aggregato o meno.

È un aggregato se ha:

  • nessun costrutto fornito dall’utente (una funzione cancellata o predefinita non conta come fornita dall’utente),
  • nessun membro di dati non statici privato o protetto,
  • nessun inizializzatore brace-or-equal-per membri di dati non statici (da c ++ 11 fino a (ripristinato) c ++ 14)
  • nessuna class base,
  • nessuna funzione membro virtuale.

Così:

  • negli scenari ABDE: foo è un aggregato
  • negli scenari C: foo non è un aggregato
  • scenario F:
    • in c ++ 11 non è un aggregato.
    • in c ++ 14 è un aggregato.
    • g ++ non lo ha implementato e lo tratta ancora come un non aggregato anche in C ++ 14.
      • 4.9 non implementa questo.
      • 5.2.0 fa
      • 5.2.1 ubuntu non (forse una regressione)

Gli effetti dell’inizializzazione dell’elenco di un object di tipo T sono:

  • Se T è un tipo aggregato, viene eseguita l’inizializzazione dell’aggregazione. Questo si occupa degli scenari ABDE (e F in C ++ 14)
  • Altrimenti i costruttori di T sono considerati in due fasi:
    • Tutti i costruttori che prendono std :: initializer_list …
    • altrimenti […] tutti i costruttori di T partecipano alla risoluzione di sovraccarico […] Questo si occupa di C (e F in C ++ 11)

:

Inizializzazione aggregata di un object di tipo T (scenari ABDE (F c ++ 14)):

  • Ogni membro della class non statico, nell’ordine in cui appare nella definizione della class, viene inizializzato dalla copia dalla clausola corrispondente dell’elenco di inizializzazione. (riferimento dell’array omesso)

TL; DR

Tutte queste regole possono ancora sembrare molto complicate e il mal di testa che induce. Personalmente lo semplifico molto per me stesso (se così mi sparassi ai piedi, allora così sia: immagino che trascorrerò 2 giorni in ospedale piuttosto che avere un paio di dozzine di mal di testa):

  • per un aggregato ogni membro di dati viene inizializzato dagli elementi dell’inixizzatore di lista
  • altrimenti chiama costruttore

Questo non ha battuto l’intero scopo di un costruttore cancellato?

Beh, non lo so, ma la soluzione è di non fare un aggregato. La forma più generale che non aggiunge sovraccarico e non modifica la syntax utilizzata dell’object è per renderla ereditata da una struttura vuota:

 struct dummy_t {}; struct foo : dummy_t { foo() = delete; }; foo f{}; // ERROR call to deleted constructor 

In alcune situazioni (nessun membro non statico, suppongo), un alternativa sarebbe eliminare il distruttore (ciò renderà l’object non istantaneo in qualsiasi contesto):

 struct foo { ~foo() = delete; }; foo f{}; // ERROR use of deleted function `foo::~foo()` 

Questa risposta utilizza le informazioni raccolte da:

  • Inizializzazione del valore in C ++ 14 con costruttore eliminato

  • Cosa sono gli aggregati e i POD e come / perché sono speciali?

  • Elenco di inizializzazione

  • Inizializzazione aggregata
  • Inizializzazione diretta

Mille grazie a @MM che ha aiutato a correggere e migliorare questo post.

Ciò che ti incasina è l’ inizializzazione aggregata .

Come dici tu, ci sono vantaggi e svantaggi nell’usare l’inizializzazione della lista. (Il termine “inizializzazione uniforms” non è usato dallo standard C ++).

Uno degli svantaggi è che l’inizializzazione dell’elenco si comporta in modo diverso per gli aggregati rispetto ai non aggregati. Inoltre, la definizione di aggregato cambia leggermente con ogni Standard.


Gli aggregati non vengono creati tramite un costruttore. (Tecnicamente potrebbero essere, ma questo è un buon modo per pensarci). Invece, quando si crea un aggregato, la memoria viene allocata e quindi ciascun membro viene inizializzato in ordine in base a ciò che è presente nell’inizializzatore dell’elenco.

I non aggregati vengono creati tramite costruttori e in tal caso i membri dell’inizializzatore dell’elenco sono argomenti del costruttore.

C’è in realtà un difetto di progettazione in quanto sopra: se abbiamo T t1; T t2{t1}; T t1; T t2{t1}; , quindi l’intento è di eseguire la copiatura. Tuttavia, (prima di C ++ 14) se T è un aggregato, avviene invece l’inizializzazione aggregata e il primo membro di t2 viene inizializzato con t1 .

Questo difetto è stato risolto in un rapporto sui difetti che modificava C ++ 14, quindi da ora in poi, la costruzione della copia viene controllata prima di passare all’inizializzazione degli aggregati.


La definizione di aggregato da C ++ 14 è:

Un aggregato è un array o una class (clausola 9) senza costruttori forniti dall’utente (12.1), nessun membro di dati non statici privato o protetto (clausola 11), nessuna class di base (clausola 10) e nessuna funzione virtuale (10.3 ).

In C ++ 11, un valore predefinito per un membro non statico significava che una class non era un aggregato; tuttavia è stato modificato per C ++ 14. Fornito dall’utente significa dichiarato dall’utente, ma non = default o = delete .


Se si desidera assicurarsi che la chiamata del costruttore non esegua mai inavvertitamente l’inizializzazione aggregata, è necessario utilizzare ( ) anziché { } ed evitare gli MVP in altri modi.

Questi casi sull’inizializzazione aggregata sono per molti di questi casi non intuitivi e sono stati object della proposta p1008: Proibisci gli aggregati con costruttori dichiarati dall’utente che dice:

C ++ attualmente consente di inizializzare alcuni tipi con costruttori dichiarati dall’utente tramite l’inizializzazione degli aggregati, scavalcando quei costruttori. Il risultato è un codice che è sorprendente, confuso e buggy. Questo documento propone una soluzione che rende la semantica di inizializzazione in C ++ più sicura, più uniforms e più facile da insegnare. Discutiamo anche delle modifiche alla rottura introdotte da questa correzione

e introduce alcuni esempi, che si sovrappongono bene con i casi che presenti:

 struct X { X() = delete; }; int main() { X x1; // ill-formsd - default c'tor is deleted X x2{}; // compiles! } 

Chiaramente, l’intento del costruttore cancellato è di impedire all’utente di inizializzare la class. Tuttavia, contrariamente all’intuizione, questo non funziona: l’utente può ancora inizializzare X tramite l’inizializzazione aggregata perché questo ignora completamente i costruttori. L’autore può anche cancellare esplicitamente tutto il costruttore predefinito, copiare e spostare, e ancora non riesce a impedire al codice client di creare un’istanza X tramite l’inizializzazione aggregata come sopra. La maggior parte degli sviluppatori C ++ sono sorpresi dal comportamento corrente quando viene mostrato questo codice L’autore della class X potrebbe in alternativa considerare di rendere privato il costruttore predefinito. Ma se a questo costruttore viene assegnata una definizione predefinita, ciò non impedisce di nuovo l’inizializzazione dell’aggregazione (e quindi l’istanziazione) della class:

 struct X { private: X() = default; }; int main() { X x1; // ill-formsd - default c'tor is private X x2{}; // compiles! } 

A causa delle regole attuali, l’inizializzazione aggregata ci consente di “build in modo predefinito” una class anche se non è, di fatto, costruibile in modo predefinito:

  static_assert(!std::is_default_constructible_v); 

passerebbe per entrambe le definizioni di X sopra.

Le modifiche proposte sono:

Modificare [dcl.init.aggr] paragrafo 1 come segue:

Un aggregato è un array o una class (clausola 12) con

  • nessun costrutto utente, esplicito , dichiarato dall’utente o ereditato (15.1),

  • nessun membro di dati non statici privato o protetto (clausola 14),

  • nessuna funzione virtuale (13.3), e

  • nessuna class base virtuale, privata o protetta (13.1).

Modificare [dcl.init.aggr] paragrafo 17 come segue:

[Nota: un array aggregato o una class aggregata possono contenere elementi di un tipo di class >> con a fornita dall’utente costruttore dichiarato dall’utente (15.1). L’inizializzazione di >> questi oggetti aggregati è descritta in 15.6.1. -End note]

Aggiungere quanto segue a [diff.cpp17] nell’allegato C, sezione C.5 C ++ e ISO C ++ 2017:

C.5.6 Clausola 11: dichiarators [diff.cpp17.dcl.decl]

Subclausa interessata : [dcl.init.aggr]
Modifica : una class che ha costruttori dichiarati dall’utente non è mai un aggregato.
Motivazione : rimuovere l’inizializzazione degli aggregati potenzialmente soggetta a errori che potrebbe applicarsi a dispetto dei costruttori dichiarati di una class.
Effetto sulla caratteristica originale : il codice valido C ++ 2017 che aggrega-inizializza un tipo con un costruttore dichiarato dall’utente può essere mal formato o avere semantica diversa in questo standard internazionale.

Seguito da esempi che ometto.

La proposta è stata accettata e unita al C ++ 20 possiamo trovare l’ ultima bozza qui che contiene queste modifiche e possiamo vedere le modifiche a [dcl.init.aggr] p1.1 e [dcl.init.aggr] p17 e C ++ 17 dichiarazioni diff .

Quindi questo dovrebbe essere risolto in C ++ 20 in avanti.