Wie unterscheidet sich "= default" von "{}" für den Standardkonstruktor und -destruktor?

169

Ich habe dies ursprünglich nur als Frage zu Destruktoren gepostet, aber jetzt füge ich die Berücksichtigung des Standardkonstruktors hinzu. Hier ist die ursprüngliche Frage:

Wenn ich meiner Klasse einen Destruktor geben möchte, der virtuell ist, aber ansonsten dem entspricht, was der Compiler generieren würde, kann ich Folgendes verwenden =default:

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

Aber es scheint, dass ich mit weniger Eingabe mit einer leeren Definition den gleichen Effekt erzielen kann:

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

Gibt es eine Art und Weise, wie sich diese beiden Definitionen unterschiedlich verhalten?

Basierend auf den Antworten auf diese Frage scheint die Situation für den Standardkonstruktor ähnlich zu sein. Gibt es angesichts der Tatsache, dass es für Destruktoren fast keinen Bedeutungsunterschied zwischen " =default" und " {}" gibt, in ähnlicher Weise fast keinen Bedeutungsunterschied zwischen diesen Optionen für Standardkonstruktoren? Angenommen, ich möchte einen Typ erstellen, in dem die Objekte dieses Typs sowohl erstellt als auch zerstört werden, warum sollte ich dann sagen?

Widget() = default;

anstatt

Widget() {}

?

Ich entschuldige mich, wenn die Erweiterung dieser Frage nach der ursprünglichen Veröffentlichung gegen einige SO-Regeln verstößt. Das Posten einer fast identischen Frage für Standardkonstruktoren schien mir die weniger wünschenswerte Option zu sein.

KnowItAllWannabe
quelle
1
Nicht das ich wüsste, aber es = defaultist expliziter imo und stimmt mit der Unterstützung durch Konstruktoren überein.
Chris
11
Ich weiß es nicht genau, aber ich denke, der erstere entspricht der Definition des "trivialen Destruktors", während der letztere dies nicht tut. So std::has_trivial_destructor<Widget>::valueist es truefür den ersten, aber falsefür den zweiten. Was die Auswirkungen davon sind, weiß ich auch nicht. :)
GManNickG
10
Ein virtueller Destruktor ist niemals trivial.
Luc Danton
@ LucDanton: Ich nehme an, meine Augen zu öffnen und den Code zu betrachten würde auch funktionieren! Danke für die Korrektur.
GManNickG
Siehe auch
Gabriel Staples

Antworten:

103

Dies ist eine völlig andere Frage, wenn nach Konstruktoren gefragt wird als nach Destruktoren.

Wenn Ihr Destruktor ist virtual, dann ist der Unterschied vernachlässigbar, wie Howard betonte . Wenn Ihr Destruktor jedoch nicht virtuell war , ist dies eine ganz andere Geschichte. Gleiches gilt für Konstruktoren.

Die Verwendung der = defaultSyntax für spezielle Elementfunktionen (Standardkonstruktor, Kopieren / Verschieben von Konstruktoren / Zuweisung, Destruktoren usw.) bedeutet etwas ganz anderes als das einfache Ausführen {}. Mit letzterem wird die Funktion "vom Benutzer bereitgestellt". Und das ändert alles.

Dies ist eine triviale Klasse nach der Definition von C ++ 11:

struct Trivial
{
  int foo;
};

Wenn Sie versuchen, ein Standardkonstrukt zu erstellen, generiert der Compiler automatisch einen Standardkonstruktor. Gleiches gilt für Kopieren / Verschieben und Zerstören. Da der Benutzer keine dieser Elementfunktionen bereitgestellt hat, wird dies in der C ++ 11-Spezifikation als "triviale" Klasse betrachtet. Es ist daher legal, dies zu tun, wie ihren Inhalt zu merken, um sie zu initialisieren und so weiter.

Dies:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Wie der Name schon sagt, ist dies nicht mehr trivial. Es verfügt über einen vom Benutzer bereitgestellten Standardkonstruktor. Es ist egal, ob es leer ist; Für die Regeln von C ++ 11 kann dies kein trivialer Typ sein.

Dies:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Wie der Name schon sagt, handelt es sich um einen trivialen Typ. Warum? Weil Sie den Compiler angewiesen haben, den Standardkonstruktor automatisch zu generieren. Der Konstruktor wird daher nicht "vom Benutzer bereitgestellt". Daher gilt der Typ als trivial, da er keinen vom Benutzer bereitgestellten Standardkonstruktor enthält.

Die = defaultSyntax dient hauptsächlich dazu, beispielsweise Konstruktoren / Zuweisungen zu kopieren, wenn Sie Elementfunktionen hinzufügen, die die Erstellung solcher Funktionen verhindern. Es löst aber auch ein spezielles Verhalten des Compilers aus, sodass es auch in Standardkonstruktoren / -destruktoren nützlich ist.

Nicol Bolas
quelle
2
Das Hauptproblem scheint also zu sein, ob die resultierende Klasse trivial ist, und diesem Problem liegt der Unterschied zwischen einer vom Benutzer deklarierten Sonderfunktion (was für =defaultFunktionen der Fall ist) und vom Benutzer bereitgestellten (was für {}Funktionen der Fall ist ) Funktionen zugrunde. Sowohl vom Benutzer deklarierte als auch vom Benutzer bereitgestellte Funktionen können die Generierung anderer spezieller Elementfunktionen verhindern (z. B. verhindert ein vom Benutzer deklarierter Destruktor die Generierung der Verschiebungsoperationen), aber nur eine vom Benutzer bereitgestellte spezielle Funktion macht eine Klasse nicht trivial. Richtig?
KnowItAllWannabe
@KnowItAllWannabe: Das ist die allgemeine Idee, ja.
Nicol Bolas
Ich wähle dies als akzeptierte Antwort, nur weil es sowohl Konstruktoren als auch (unter Bezugnahme auf Howards Antwort) Destruktoren abdeckt.
KnowItAllWannabe
Scheint hier ein fehlendes Wort zu sein "Was die Regeln von C ++ 11 betrifft, haben Sie die Rechte eines trivialen Typs" Ich würde es beheben, aber ich bin nicht 100% sicher, was beabsichtigt war.
Jcoder
2
= defaultscheint nützlich zu sein, um den Compiler zu zwingen, trotz der Anwesenheit anderer Konstruktoren einen Standardkonstruktor zu generieren; Der Standardkonstruktor wird nicht implizit deklariert, wenn andere vom Benutzer deklarierte Konstruktoren bereitgestellt werden.
bgfvdu3w
42

Sie sind beide nicht trivial.

Sie haben beide die gleiche Noexcept-Spezifikation, abhängig von der Noexcept-Spezifikation der Basen und Mitglieder.

Der einzige Unterschied, den ich bisher feststelle, besteht darin, dass, wenn er Widgeteine Basis oder ein Mitglied mit einem unzugänglichen oder gelöschten Destruktor enthält:

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

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

Dann wird die =defaultLösung kompiliert, ist aber Widgetkein zerstörbarer Typ. Wenn Sie also versuchen, a zu zerstören Widget, wird ein Fehler beim Kompilieren angezeigt. Wenn Sie dies nicht tun, haben Sie ein Arbeitsprogramm.

Otoh, wenn Sie den vom Benutzer bereitgestellten Destruktor bereitstellen, werden die Dinge nicht kompiliert, unabhängig davon, ob Sie Folgendes zerstören 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.
Howard Hinnant
quelle
9
Interessant: Mit anderen Worten, mit =default;dem Compiler wird der Destruktor nur generiert, wenn er verwendet wird, und daher wird kein Fehler ausgelöst. Das kommt mir komisch vor, auch wenn es nicht unbedingt ein Fehler ist. Ich kann mir nicht vorstellen , dass dieses Verhalten wird beauftragt , in der Norm.
Nik Bougalis
"Dann wird die = Standardlösung kompiliert" Nein, wird es nicht. Gerade getestet in vs.
Nano
Was war die Fehlermeldung und welche Version von VS?
Howard Hinnant
35

Der wichtige Unterschied zwischen

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

und

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

ist, dass der mit definierte Standardkonstruktor B() = default;als nicht benutzerdefiniert betrachtet wird . Dies bedeutet, dass im Falle einer Wertinitialisierung wie in

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

Eine spezielle Art der Initialisierung, bei der überhaupt kein Konstruktor verwendet wird, findet statt. Bei integrierten Typen führt dies zu einer Nullinitialisierung . In diesem Fall B(){}findet dies nicht statt. Der C ++ Standard n3337 § 8.5 / 7 sagt

Ein Objekt vom Typ T zu initialisieren bedeutet:

- Wenn T ein (möglicherweise lebenslaufqualifizierter) Klassentyp (Abschnitt 9) mit einem vom Benutzer bereitgestellten Konstruktor (12.1) ist, wird der Standardkonstruktor für T aufgerufen (und die Initialisierung ist fehlerhaft, wenn T keinen zugänglichen Standardkonstruktor hat );

- Wenn T ein (möglicherweise lebenslaufqualifizierter) Nicht-Union-Klassentyp ohne einen vom Benutzer bereitgestellten Konstruktor ist , wird das Objekt mit Null initialisiert, und wenn der implizit deklarierte Standardkonstruktor von T nicht trivial ist, wird dieser Konstruktor aufgerufen.

- Wenn T ein Array-Typ ist, wird jedes Element mit einem Wert initialisiert. - Andernfalls wird das Objekt mit Null initialisiert.

Beispielsweise:

#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;
}

mögliches Ergebnis:

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

http://ideone.com/k8mBrd

4pie0
quelle
Warum initialisieren "{}" und "= default" dann immer einen std :: string ideone.com/LMv5Uf ?
Nawfel Bgh
1
@nawfelbgh Der Standardkonstruktor A () {} ruft den Standardkonstruktor für std :: string auf, da dies kein POD-Typ ist. Der Standard-Ctor von std :: string initialisiert ihn als leere Zeichenfolge mit der Größe 0. Der Standard-Ctor für Skalare bewirkt nichts: Die Objekte mit automatischer Speicherdauer (und ihre Unterobjekte) werden auf unbestimmte Werte initialisiert.
4pie0