La nuova sintassi "= default" in C ++ 11


136

Non capisco perché mai dovrei farlo:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Perché non dire semplicemente:

S() {} // instead of S() = default;

perché introdurre una nuova sintassi per questo?


30
Nitpick: defaultnon è una nuova parola chiave, è semplicemente un nuovo uso di una parola chiave già riservata.


Mey be Questa domanda potrebbe aiutarti.
FreeNickname

7
Oltre alle altre risposte, direi anche che '= default;' è più auto-documentante.
Segna il

Risposte:


136

Un costruttore predefinito predefinito è specificamente definito come uguale a un costruttore predefinito definito dall'utente senza un elenco di inizializzazione e un'istruzione composta vuota.

§12.1 / 6 [class.ctor] Un costruttore predefinito che è predefinito e non definito come cancellato è implicitamente definito quando viene usato per creare un oggetto del suo tipo di classe o quando è esplicitamente predefinito dopo la sua prima dichiarazione. Il costruttore predefinito definito implicitamente esegue l'insieme di inizializzazioni della classe che verrebbe eseguito da un costruttore predefinito scritto dall'utente per quella classe senza inizializzatore ctor (12.6.2) e un'istruzione composta vuota. [...]

Tuttavia, mentre entrambi i costruttori si comporteranno allo stesso modo, fornire un'implementazione vuota influisce su alcune proprietà della classe. Dare un costruttore definito dall'utente, anche se non fa nulla, rende il tipo non un aggregato e anche non banale . Se vuoi che la tua classe sia un tipo aggregato o banale (o per transitività, un tipo POD), allora devi usare = default.

§8.5.1 / 1 [dcl.init.aggr] Un aggregato è un array o una classe senza costruttori forniti dall'utente, [e ...]

§12.1 / 5 [class.ctor] Un costruttore predefinito è banale se non è fornito dall'utente e [...]

§9 / 6 [classe] Una classe banale è una classe che ha un costruttore predefinito banale e [...]

Dimostrare:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

Inoltre, il default esplicito di un costruttore lo renderà constexprse il costruttore implicito fosse stato e gli darà anche la stessa specifica di eccezione che il costruttore implicito avrebbe avuto. Nel caso che hai indicato, il costruttore implicito non sarebbe stato constexpr(perché lascerebbe un membro di dati non inizializzato) e avrebbe anche una specifica di eccezione vuota, quindi non c'è differenza. Sì, in generale è possibile specificare manualmente constexpre la specifica dell'eccezione in modo che corrisponda al costruttore implicito.

L'uso = defaultporta un po 'di uniformità, perché può essere utilizzato anche con costruttori e distruttori di copia / spostamento. Un costruttore di copie vuoto, ad esempio, non farà lo stesso di un costruttore di copie predefinito (che eseguirà una copia dei membri dei membri). L'uso della sintassi = default(o = delete) in modo uniforme per ciascuna di queste funzioni speciali per i membri semplifica la lettura del codice dichiarando esplicitamente le tue intenzioni.


Quasi. 12.1 / 6: "Se quel costruttore predefinito scritto dall'utente soddisfa i requisiti di un constexprcostruttore (7.1.5), lo è il costruttore predefinito implicitamente definito constexpr."
Casey,

In realtà, 8.4.2 / 2 è più informativo: "Se una funzione è esplicitamente inadempiuta nella sua prima dichiarazione, (a) è implicitamente considerata come constexprse la dichiarazione implicita fosse, (b) è implicitamente considerata la stessa specifica dell'eccezione come se fosse stata implicitamente dichiarata (15.4), ... "Non fa differenza in questo caso specifico, ma in generalefoo() = default; presenta un leggero vantaggio foo() {}.
Casey,

2
Dici che non c'è differenza, e poi continui a spiegare le differenze?

@hvd In questo caso non c'è alcuna differenza, perché la dichiarazione implicita non lo sarebbe constexpr(poiché un membro di dati non viene inizializzato) e la sua specifica di eccezione consente tutte le eccezioni. Lo chiarirò.
Joseph Mansfield,

2
Grazie per il chiarimento. Sembra tuttavia che ci sia ancora una differenza con constexpr(che hai menzionato non dovrebbe fare la differenza qui): dà struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};solo s1un errore, no s2. Sia in clang che in g ++.

10

Ho un esempio che mostrerà la differenza:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

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


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

Produzione:

5
5
0

Come possiamo vedere, la chiamata per il costruttore A () vuoto non inizializza i membri, mentre B () lo fa.


7
potresti spiegare questa sintassi -> new (& x) A ();
Vencat,

5
Stiamo creando un nuovo oggetto nella memoria iniziato dall'indirizzo della variabile x (anziché dalla nuova allocazione di memoria). Questa sintassi viene utilizzata per creare oggetti nella memoria pre-allocata. Come nel nostro caso la dimensione di B = la dimensione di int, quindi new (& x) A () creerà un nuovo oggetto al posto della variabile x.
Slavenskij,

Grazie per la tua spiegazione.
Vencat,

1
Ottengo risultati diversi con gcc 8.3: ideone.com/XouXux
Adam.Er8

Anche con C ++ 14, sto ottenendo risultati diversi: ideone.com/CQphuT
Mayank Bhushan

9

N2210 fornisce alcuni motivi:

La gestione delle impostazioni predefinite presenta diversi problemi:

  • Le definizioni del costruttore sono accoppiate; la dichiarazione di qualsiasi costruttore elimina il costruttore predefinito.
  • L'impostazione predefinita del distruttore è inappropriata rispetto alle classi polimorfiche e richiede una definizione esplicita.
  • Una volta soppresso un valore predefinito, non è più possibile ripristinarlo.
  • Le implementazioni predefinite sono spesso più efficienti delle implementazioni specificate manualmente.
  • Le implementazioni non predefinite sono non banali, il che influisce sulla semantica dei tipi, ad esempio rende un tipo non POD.
  • Non è possibile vietare una funzione di membro speciale o un operatore globale senza dichiarare un sostituto (non banale).

type::type() = default;
type::type() { x = 3; }

In alcuni casi, il corpo della classe può cambiare senza richiedere una modifica nella definizione della funzione membro poiché l'impostazione predefinita cambia con la dichiarazione di membri aggiuntivi.

Vedi Rule-of-Three diventa Rule-of-Five con C ++ 11? :

Si noti che il costruttore di spostamento e l'operatore di assegnazione di spostamento non verranno generati per una classe che dichiara esplicitamente nessuna delle altre funzioni membro speciali, che il costruttore di copia e l'operatore di assegnazione di copia non verranno generati per una classe che dichiara esplicitamente un costruttore di spostamento o uno spostamento operatore di assegnazione e che una classe con un distruttore dichiarato esplicitamente e un costruttore di copia implicitamente definito o un operatore di assegnazione di copia definito implicitamente è considerata obsoleta


1
Sono ragioni per avere = defaultin generale, piuttosto che ragioni per fare = defaultsu un costruttore contro fare { }.
Joseph Mansfield,

@JosephMansfield Vero, ma poiché {}era già una caratteristica del linguaggio prima dell'introduzione di =default, queste ragioni si basano implicitamente sulla distinzione (ad es. "Non esiste alcun modo per resuscitare [un valore predefinito soppresso]" implica che non{} è equivalente al valore predefinito ).
Kyle Strand,

7

È una questione di semantica in alcuni casi. Non è molto ovvio con i costruttori predefiniti, ma diventa evidente con altre funzioni membro generate dal compilatore.

Per il costruttore predefinito, sarebbe stato possibile rendere qualsiasi costruttore predefinito con un corpo vuoto essere considerato un candidato per essere un costruttore banale, come usare =default. Dopotutto, i vecchi costruttori predefiniti vuoti erano C ++ legali .

struct S { 
  int a; 
  S() {} // legal C++ 
};

Il fatto che il compilatore capisca o meno che questo costruttore sia banale è irrilevante nella maggior parte dei casi al di fuori delle ottimizzazioni (manuali o del compilatore).

Tuttavia, questo tentativo di considerare i corpi di funzione vuoti come "predefiniti" si interrompe completamente per altri tipi di funzioni membro. Considera il costruttore della copia:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

Nel caso precedente, il costruttore di copie scritto con un corpo vuoto è ora sbagliato . In realtà non sta più copiando nulla. Questo è un insieme molto diverso di semantica rispetto alla semantica predefinita del costruttore di copie. Il comportamento desiderato richiede di scrivere del codice:

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

Anche con questo semplice caso, tuttavia, sta diventando molto più un onere per il compilatore verificare che il costruttore di copie sia identico a quello che si genererebbe o per vedere che il costruttore di copie è banale (equivalente a unmemcpy , sostanzialmente ). Il compilatore dovrebbe controllare l'espressione di ogni inizializzatore del membro e assicurarsi che sia identico all'espressione per accedere al membro corrispondente del sorgente e nient'altro, assicurarsi che nessun membro rimanga con una costruzione predefinita non banale, ecc. È indietro in un modo del processo il compilatore userebbe per verificare che le proprie versioni generate di questa funzione siano banali.

Considera quindi l'operatore di assegnazione delle copie che può diventare ancora più peloso, specialmente nel caso non banale. È una tonnellata di boiler-plate che non vuoi scrivere per molte classi, ma sei comunque obbligato a farlo in C ++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

Questo è un caso semplice, ma è già più codice di quanto vorresti mai essere costretto a scrivere per un tipo così semplice come T(specialmente una volta che lanciamo operazioni di spostamento nel mix). Non possiamo fare affidamento su un corpo vuoto che significa "riempire i valori predefiniti" perché il corpo vuoto è già perfettamente valido e ha un significato chiaro. In effetti, se il corpo vuoto fosse usato per indicare "riempire i valori predefiniti", non ci sarebbe modo di creare esplicitamente un costruttore di copie non operative o simili.

È di nuovo una questione di coerenza. Il corpo vuoto significa "non fare nulla" ma per cose come i costruttori di copie non vuoi davvero "non fare nulla" ma piuttosto "fai tutte le cose che normalmente faresti se non fossero soppresse". Quindi =default. È necessario per superare funzioni membro generate dal compilatore soppresse come costruttori di copia / spostamento e operatori di assegnazione. È quindi "ovvio" farlo funzionare anche per il costruttore predefinito.

Sarebbe stato bello rendere il costruttore predefinito con corpi vuoti e anche i costruttori banali di membri / base fossero considerati banali proprio come lo sarebbero stati =defaultse solo per rendere il codice più vecchio più ottimale in alcuni casi, ma la maggior parte del codice di basso livello si basava su banale i costruttori predefiniti per le ottimizzazioni si basano anche su costruttori di copie banali. Se devi andare a "riparare" tutti i tuoi vecchi costruttori di copie, in realtà non è un granché dover riparare tutti i tuoi vecchi costruttori predefiniti. È anche molto più chiaro e più ovvio usare un esplicito =defaultper indicare le tue intenzioni.

Ci sono alcune altre cose che faranno le funzioni membro generate dal compilatore che dovresti anche fare esplicitamente modifiche per supportare. Il supporto constexprper i costruttori predefiniti è un esempio. È solo più facile da usare mentalmente =defaultche dover contrassegnare le funzioni con tutte le altre parole chiave speciali e tali che sono implicite =defaulte che era uno dei temi di C ++ 11: rendere il linguaggio più semplice. Ha ancora molte verruche e compromessi per la retrocompatibilità, ma è chiaro che è un grande passo avanti rispetto a C ++ 03 quando si tratta di facilità d'uso.


Ho avuto un problema che mi aspettavo = defaultpotesse fare a=0;e non lo era! Ho dovuto lasciarlo cadere a favore di : a(0). Sono ancora confuso su quanto sia utile = default, si tratta di prestazioni? si romperà da qualche parte se non lo uso = default? Ho provato a leggere tutte le risposte qui acquista Sono nuovo ad alcune cose in c ++ e ho molti problemi a capirlo.
Aquarius Power il

@AquariusPower: non si tratta "solo" di prestazioni, ma in alcuni casi è richiesto anche in caso di eccezioni e altre semantiche. Vale a dire, un operatore predefinito può essere banale, ma un operatore non predefinito non può mai essere banale, e alcuni codici useranno tecniche di meta-programmazione per modificare il comportamento o addirittura impedire tipi con operazioni non banali. Il tuo a=0esempio è a causa del comportamento di tipi banali, che sono un argomento separato (anche se correlato).
Sean Middleditch,

vuol dire che è possibile avere = defaulte ancora alo sarà =0? in qualche modo? pensi che potrei creare una nuova domanda come "come avere un costruttore = defaulte concedere che i campi vengano inizializzati correttamente?", tra l'altro ho avuto il problema in a structe non a class, e l'app funziona correttamente anche se non lo uso = default, posso aggiungi una struttura minima a questa domanda se è buona :)
Aquarius Power il

1
@AquariusPower: è possibile utilizzare inizializzatori di dati non statici. Scrivi la tua struttura in questo modo: struct { int a = 0; };se poi decidi di aver bisogno di un costruttore, potresti impostarlo come predefinito, ma nota che il tipo non sarà banale (il che va bene).
Sean Middleditch,

2

A causa della deprecazione std::is_pode della sua alternativa std::is_trivial && std::is_standard_layout, lo snippet della risposta di @JosephMansfield diventa:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

Si noti che Yè ancora di layout standard.

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.