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?
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?
Risposte:
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à constexpr
se 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 constexpr
e la specifica dell'eccezione in modo che corrisponda al costruttore implicito.
L'uso = default
porta 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.
constexpr
costruttore (7.1.5), lo è il costruttore predefinito implicitamente definito constexpr
."
constexpr
se 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() {}
.
constexpr
(poiché un membro di dati non viene inizializzato) e la sua specifica di eccezione consente tutte le eccezioni. Lo chiarirò.
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 s1
un errore, no s2
. Sia in clang che in g ++.
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.
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
= default
in generale, piuttosto che ragioni per fare = default
su un costruttore contro fare { }
.
{}
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 ).
È 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 =default
se 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 =default
per 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 constexpr
per i costruttori predefiniti è un esempio. È solo più facile da usare mentalmente =default
che dover contrassegnare le funzioni con tutte le altre parole chiave speciali e tali che sono implicite =default
e 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.
= default
potesse 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.
a=0
esempio è a causa del comportamento di tipi banali, che sono un argomento separato (anche se correlato).
= default
e ancora a
lo sarà =0
? in qualche modo? pensi che potrei creare una nuova domanda come "come avere un costruttore = default
e concedere che i campi vengano inizializzati correttamente?", tra l'altro ho avuto il problema in a struct
e non a class
, e l'app funziona correttamente anche se non lo uso = default
, posso aggiungi una struttura minima a questa domanda se è buona :)
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).
A causa della deprecazione std::is_pod
e 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.
default
non è una nuova parola chiave, è semplicemente un nuovo uso di una parola chiave già riservata.