In che modo “= default” è diverso da “{}” per costruttore e distruttore predefiniti?


169

Inizialmente ho pubblicato questo come una domanda solo sui distruttori, ma ora sto aggiungendo considerazione sul costruttore predefinito. Ecco la domanda originale:

Se voglio dare alla mia classe un distruttore virtuale, ma altrimenti uguale a quello che genererebbe il compilatore, posso usare =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Ma sembra che posso ottenere lo stesso effetto con meno digitazioni usando una definizione vuota:

class Widget {
public:
   virtual ~Widget() {}
};

Esiste un modo in cui queste due definizioni si comportano diversamente?

Sulla base delle risposte pubblicate per questa domanda, la situazione per il costruttore predefinito sembra simile. Dato che non vi è quasi alcuna differenza di significato tra " =default" e " {}" per i distruttori, allo stesso modo non esiste alcuna differenza di significato tra queste opzioni per i costruttori predefiniti? Cioè, supponendo che io voglia creare un tipo in cui gli oggetti di quel tipo saranno sia creati che distrutti, perché dovrei dire

Widget() = default;

invece di

Widget() {}

?

Mi scuso se l'estensione di questa domanda dopo la sua pubblicazione originale sta violando alcune regole SO. Pubblicare una domanda quasi identica per i costruttori predefiniti mi è sembrata l'opzione meno desiderabile.


1
Non che io sappia, ma = defaultè più esplicito imo, ed è coerente con il supporto per esso con i costruttori.
chris,

11
Non lo so per certo, ma penso che il primo sia conforme alla definizione di "banale distruttore", mentre il secondo no. Lo stesso std::has_trivial_destructor<Widget>::valuevale trueper il primo, ma falseper il secondo. Quali siano le implicazioni di ciò non lo so neanche io. :)
GManNickG

10
Un distruttore virtuale non è mai banale.
Luc Danton,

@LucDanton: suppongo che anche aprire gli occhi e guardare il codice funzionerebbe! Grazie per la correzione.
GManNickG,

Risposte:


103

Questa è una domanda completamente diversa quando si chiedono costruttori che distruttori.

Se il tuo distruttore lo è virtual, allora la differenza è trascurabile, come ha sottolineato Howard . Tuttavia, se il distruttore non era virtuale , è una storia completamente diversa. Lo stesso vale per i costruttori.

L'uso della = defaultsintassi per funzioni speciali dei membri (costruttore predefinito, copia / sposta costruttori / assegnazione, distruttori ecc.) Significa qualcosa di molto diverso dal semplice fare {}. Con quest'ultimo, la funzione diventa "fornita dall'utente". E questo cambia tutto.

Questa è una banale classe secondo la definizione di C ++ 11:

struct Trivial
{
  int foo;
};

Se si tenta di costruirne uno predefinito, il compilatore genererà automaticamente un costruttore predefinito. Lo stesso vale per copia / movimento e distruzione. Poiché l'utente non ha fornito nessuna di queste funzioni membro, la specifica C ++ 11 considera questa classe "banale". È quindi legale farlo, come memorizzare i loro contenuti per inizializzarli e così via.

Questo:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Come suggerisce il nome, questo non è più banale. Ha un costruttore predefinito fornito dall'utente. Non importa se è vuoto; per quanto riguarda le regole di C ++ 11, questo non può essere un tipo banale.

Questo:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Ancora una volta, come suggerisce il nome, questo è un tipo banale. Perché? Perché hai detto al compilatore di generare automaticamente il costruttore predefinito. Il costruttore non è quindi "fornito dall'utente". E quindi, il tipo conta come banale, dal momento che non ha un costruttore predefinito fornito dall'utente.

La = defaultsintassi è principalmente lì per fare cose come costruttori / assegnazioni di copie, quando si aggiungono funzioni membro che impediscono la creazione di tali funzioni. Ma attiva anche un comportamento speciale dal compilatore, quindi è utile anche nei costruttori / distruttori predefiniti.


2
Quindi il problema chiave sembra essere se la classe risultante è banale, e alla base di tale problema c'è la differenza tra una funzione speciale dichiarata dall'utente (che è il caso delle =defaultfunzioni) e le funzioni fornite dall'utente (che è il caso {}). Entrambe le funzioni dichiarate dall'utente e fornite dall'utente possono impedire la generazione di altre funzioni membro speciali (ad esempio, un distruttore dichiarato dall'utente impedisce la generazione delle operazioni di spostamento), ma solo una funzione speciale fornita dall'utente rende una classe non banale. Destra?
KnowItAllWannabe,

@KnowItAllWannabe: Questa è l'idea generale, sì.
Nicol Bolas,

Sto scegliendo questa come risposta accettata, solo perché copre sia i costruttori che (in riferimento alla risposta di Howard) i distruttori.
KnowItAllWannabe,

Sembra essere una parola mancante qui "per quanto riguarda le regole di C ++ 11, voi i diritti di un tipo banale" Lo risolverei ma non sono sicuro al 100% di ciò che era destinato.
jcoder,

2
= defaultsembra essere utile per forzare il compilatore a generare un costruttore predefinito nonostante la presenza di altri costruttori; il costruttore predefinito non viene dichiarato implicitamente se vengono forniti altri costruttori dichiarati dall'utente.
bgfvdu3w,

42

Sono entrambi non banali.

Entrambi hanno la stessa specifica noexcept a seconda della specifica noexcept delle basi e dei membri.

L'unica differenza che sto rilevando finora è che se Widgetcontiene una base o un membro con un distruttore inaccessibile o eliminato:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Quindi la =defaultsoluzione verrà compilata, ma Widgetnon sarà un tipo distruttibile. Cioè se provi a distruggere un Widget, otterrai un errore in fase di compilazione. Ma se non lo fai, hai un programma di lavoro.

Otoh, se fornisci il distruttore fornito dall'utente , le cose non si compileranno se distruggi o meno un Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.

9
Interessante: in altre parole, con =default;il compilatore non verrà generato il distruttore a meno che non venga utilizzato, e quindi non causerà un errore. Mi sembra strano, anche se non necessariamente un bug. Non riesco a immaginare che questo comportamento sia obbligatorio nello standard.
Nik Bougalis,

"Quindi la = soluzione predefinita verrà compilata" No, non lo farà. Ho appena provato contro
nano il

Qual era il messaggio di errore e quale versione di VS?
Howard Hinnant,

35

L'importante differenza tra

class B {
    public:
    B(){}
    int i;
    int j;
};

e

class B {
    public:
    B() = default;
    int i;
    int j;
};

è che il costruttore predefinito definito con B() = default;è considerato non definito dall'utente . Ciò significa che in caso di inizializzazione del valore come in

B* pb = new B();  // use of () triggers value-initialization

avrà luogo un tipo speciale di inizializzazione che non utilizza affatto un costruttore e per i tipi predefiniti ciò comporterà un'inizializzazione zero . In questo caso B(){}non avverrà. Lo standard C ++ n3337 § 8.5 / 7 dice

Inizializzare un oggetto di tipo T significa:

- se T è un tipo di classe (possibilmente qualificato in cv) (clausola 9) con un costruttore fornito dall'utente (12.1), viene chiamato il costruttore predefinito per T (e l'inizializzazione non è corretta se T non ha un costruttore predefinito accessibile );

- se T è un tipo di classe non sindacale (possibilmente qualificato in cv) senza un costruttore fornito dall'utente , allora l'oggetto è inizializzato a zero e, se il costruttore predefinito dichiarato implicitamente di T è non banale, viene chiamato quel costruttore.

- se T è un tipo di array, ogni elemento è inizializzato dal valore; - in caso contrario, l'oggetto viene inizializzato con zero.

Per esempio:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

risultato possibile:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd


Quindi, perché "{}" e "= default" inizializzano sempre un ideone :: string ideone.com/LMv5Uf ?
nawfel dal

1
@nawfelbgh Il costruttore predefinito A () {} chiama il costruttore predefinito per std :: string poiché si tratta di un tipo non POD. Il ctor predefinito di std :: string lo inizializza in una stringa di dimensioni 0 vuota. Il ctor predefinito per gli scalari non fa nulla: gli oggetti con durata di memorizzazione automatica (e i loro oggetti secondari) sono inizializzati su valori indeterminati.
4pie0,
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.