Warum benötigt C ++ einen vom Benutzer bereitgestellten Standardkonstruktor, um ein const-Objekt standardmäßig zu erstellen?

99

Der C ++ - Standard (Abschnitt 8.5) lautet:

Wenn ein Programm die Standardinitialisierung eines Objekts eines const-qualifizierten Typs T anfordert, muss T ein Klassentyp mit einem vom Benutzer bereitgestellten Standardkonstruktor sein.

Warum? Ich kann mir keinen Grund vorstellen, warum in diesem Fall ein vom Benutzer bereitgestellter Konstruktor erforderlich ist.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}
Karu
quelle
2
Die Zeile scheint in Ihrem Beispiel nicht erforderlich zu sein (siehe ideone.com/qqiXR ), da Sie deklariert, aber nicht definiert / initialisiert haben a, aber gcc-4.3.4 akzeptiert sie auch dann, wenn Sie dies tun (siehe ideone.com/uHvFS ).
Ray Toal
Das obige Beispiel deklariert und definiert a. Comeau erzeugt einen Fehler "const variable" a "erfordert einen Initialisierer - Klasse" A "hat keinen explizit deklarierten Standardkonstruktor", wenn die Zeile auskommentiert ist.
Karu
4
Dies ist in C ++ 11 behoben, können Sie schreiben const A a{}:)
Howard Lovatt

Antworten:

10

Dies wurde als Fehler angesehen (gegenüber allen Versionen des Standards) und vom Defekt 253 der Core Working Group (CWG) behoben . Der neue Wortlaut für die Standardzustände in http://eel.is/c++draft/dcl.init#7

Ein Klassentyp T ist const-default-constructible, wenn die Standardinitialisierung von T einen vom Benutzer bereitgestellten Konstruktor von T aufrufen würde (nicht von einer Basisklasse geerbt) oder wenn

  • Jedes direkte nichtvariante nicht statische Datenelement M von T hat einen Standardelementinitialisierer, oder wenn M vom Klassentyp X (oder einem Array davon) ist, ist X const-default-konstruierbar.
  • Wenn T eine Vereinigung mit mindestens einem nicht statischen Datenelement ist, hat genau ein Variantenelement einen Standardelementinitialisierer.
  • Wenn T keine Vereinigung ist, hat für jedes anonyme Gewerkschaftsmitglied mit mindestens einem nicht statischen Datenelement (falls vorhanden) genau ein nicht statisches Datenelement einen Standardelementinitialisierer und
  • Jede potenziell konstruierte Basisklasse von T ist const-default-konstruierbar.

Wenn ein Programm die Standardinitialisierung eines Objekts eines const-qualifizierten Typs T fordert, muss T ein const-default-konstruierbarer Klassentyp oder ein Array davon sein.

Dieser Wortlaut bedeutet im Wesentlichen, dass der offensichtliche Code funktioniert. Wenn Sie alle Ihre Basen und Mitglieder initialisieren, können Sie A const a;unabhängig davon sagen, wie oder ob Sie Konstruktoren buchstabieren.

struct A {
};
A const a;

gcc hat dies seit 4.6.4 akzeptiert. clang hat dies seit 3.9.0 akzeptiert. Visual Studio akzeptiert dies ebenfalls (zumindest 2017, nicht sicher, ob früher).

David Stone
quelle
3
Dies ist jedoch immer noch verboten struct A { int n; A() = default; }; const A a;, struct B { int n; B() {} }; const B b;da der neue Wortlaut immer noch "vom Benutzer bereitgestellt" und nicht "vom Benutzer deklariert" lautet und ich mich am Kopf kratzen muss, warum das Komitee explizit voreingestellte Standardkonstruktoren von dieser DR ausgeschlossen hat, was uns dazu zwingt Unsere Klassen sind nicht trivial, wenn wir const-Objekte mit nicht initialisierten Mitgliedern wollen.
Oktalist
1
Interessant, aber es gibt immer noch einen Randfall, auf den ich gestoßen bin. Mit MyPODeinem POD ist struct, static MyPOD x;- unter Berufung auf Null-Initialisierung (ist das der richtige?) , Um die Member - Variable zu setzen (s) in geeigneter Weise - kompiliert, aber static const MyPOD x;nicht der Fall. Gibt es eine Chance, dass das behoben wird?
Joshua Green
66

Der Grund dafür ist, dass die Klasse, wenn sie keinen benutzerdefinierten Konstruktor hat, POD sein kann und die POD-Klasse nicht standardmäßig initialisiert wird. Wenn Sie also ein nicht initialisiertes const-Objekt von POD deklarieren, welchen Nutzen hat es dann? Daher denke ich, dass der Standard diese Regel erzwingt, damit das Objekt tatsächlich nützlich sein kann.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Aber wenn Sie die Klasse zu einem Nicht-POD machen:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Beachten Sie den Unterschied zwischen POD und Nicht-POD.

Der benutzerdefinierte Konstruktor ist eine Möglichkeit, die Klasse nicht POD zu machen. Es gibt verschiedene Möglichkeiten, dies zu tun.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Beachten Sie, dass nonPOD_B keinen benutzerdefinierten Konstruktor definiert. Kompiliere es. Es wird kompiliert:

Und kommentieren Sie die virtuelle Funktion, dann gibt es wie erwartet Fehler:


Nun, ich denke, Sie haben die Passage falsch verstanden. Es sagt zuerst dies (§8.5 / 9):

Wenn für ein Objekt kein Initialisierer angegeben ist und das Objekt vom (möglicherweise lebenslaufqualifizierten) Nicht-POD-Klassentyp (oder einem Array davon) ist, muss das Objekt standardmäßig initialisiert werden. [...]

Es handelt sich um einen möglicherweise nicht lebenslaufqualifizierten Typ ohne POD-Klasse . Das heißt, das Nicht-POD-Objekt wird standardmäßig initialisiert, wenn kein Initialisierer angegeben ist. Und was ist standardmäßig initialisiert ? Für Nicht-POD heißt es in der Spezifikation (§8.5 / 5):

Die Standardinitialisierung eines Objekts vom Typ T bedeutet:
- Wenn T ein Nicht-POD-Klassentyp ist (Abschnitt 9), wird der Standardkonstruktor für T aufgerufen (und die Initialisierung ist fehlerhaft, wenn T keinen zugänglichen Standardkonstruktor hat);

Es geht einfach um den Standardkonstruktor von T, unabhängig davon, ob sein benutzerdefinierter oder vom Compiler generierter Konstruktor irrelevant ist.

Wenn Sie sich darüber im Klaren sind, verstehen Sie, was in der Spezifikation als nächstes steht ((§8.5 / 9),

[...]; Wenn das Objekt vom Typ const-qualifiziert ist, muss der zugrunde liegende Klassentyp einen vom Benutzer deklarierten Standardkonstruktor haben.

Dieser Text impliziert also, dass das Programm fehlerhaft ist, wenn das Objekt vom Typ POD mit konstanter Qualifikation ist und kein Initialisierer angegeben ist (da POD nicht standardmäßig initialisiert sind):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

Übrigens wird dies gut kompiliert , da es kein POD ist und standardmäßig initialisiert werden kann .

Nawaz
quelle
1
Ich glaube, Ihr letztes Beispiel ist ein Kompilierungsfehler - es nonPOD_Bgibt keinen vom Benutzer bereitgestellten Standardkonstruktor, daher ist die Zeile const nonPOD_B b2nicht zulässig.
Karu
1
Eine andere Möglichkeit, die Klasse zu einem Nicht-POD zu machen, besteht darin, ihr ein Datenelement zu geben, das kein POD ist (z. B. meine Struktur Bin der Frage). In diesem Fall ist der vom Benutzer bereitgestellte Standardkonstruktor weiterhin erforderlich.
Karu
"Wenn ein Programm die Standardinitialisierung eines Objekts eines const-qualifizierten Typs T anfordert, muss T ein Klassentyp mit einem vom Benutzer bereitgestellten Standardkonstruktor sein."
Karu
@ Karu: Das habe ich gelesen. Es scheint, dass die Spezifikation andere Passagen enthält, die es ermöglichen, constNicht-POD-Objekte durch Aufrufen des vom Compiler generierten Standardkonstruktors zu initialisieren.
Nawaz
2
Ihre Ideone-Links scheinen defekt zu sein, und es wäre großartig, wenn diese Antwort auf C ++ 11/14 aktualisiert werden könnte, da in §8.5 POD überhaupt nicht erwähnt wird.
Oktalist
12

Reine Spekulation meinerseits, aber bedenken Sie, dass auch andere Typen eine ähnliche Einschränkung haben:

int main()
{
    const int i; // invalid
}

Diese Regel ist also nicht nur konsistent, sondern verhindert auch (rekursiv) einheitliche const(Unter-) Objekte:

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Was die andere Seite der Frage betrifft (die es für Typen mit einem Standardkonstruktor zulässt), denke ich, dass ein Typ mit einem vom Benutzer bereitgestellten Standardkonstruktor nach der Konstruktion immer in einem vernünftigen Zustand sein sollte. Beachten Sie, dass die geltenden Regeln Folgendes zulassen:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Dann könnten wir vielleicht eine Regel formulieren wie "Mindestens ein Mitglied muss in einem vom Benutzer bereitgestellten Standardkonstruktor sinnvoll initialisiert werden", aber das ist viel zu viel Zeit, um sich vor Murphy zu schützen. C ++ vertraut dem Programmierer in bestimmten Punkten.

Luc Danton
quelle
Durch Hinzufügen A(){}wird der Fehler jedoch behoben, sodass nichts verhindert wird. Die Regel funktioniert nicht rekursiv - X(){}wird für dieses Beispiel nie benötigt.
Karu
2
Nun, zumindest indem er den Programmierer zwingt, einen Konstruktor hinzuzufügen, ist er gezwungen, eine Minute über das Problem nachzudenken und vielleicht eine nicht triviale zu finden
arne
@ Karu Ich habe nur die Hälfte der Frage angesprochen - das behoben :)
Luc Danton
4
@arne: Das einzige Problem ist, dass es der falsche Programmierer ist. Die Person, die versucht, die Klasse zu instanziieren, gibt möglicherweise alle gewünschten Gedanken zu dieser Angelegenheit, kann die Klasse jedoch möglicherweise nicht ändern. Der Klassenautor dachte an die Mitglieder und stellte fest, dass sie alle vom impliziten Standardkonstruktor sinnvoll initialisiert wurden, also fügte er nie einen hinzu.
Karu
3
Was ich diesem Teil des Standards entnommen habe, ist "immer einen Standardkonstruktor für Nicht-POD-Typen deklarieren, falls jemand eines Tages eine const-Instanz erstellen möchte". Das scheint ein bisschen übertrieben.
Karu
3

Ich habe Timur Doumlers Vortrag auf dem Meeting C ++ 2018 gesehen und schließlich festgestellt, warum der Standard hier einen vom Benutzer bereitgestellten Konstruktor erfordert, nicht nur einen vom Benutzer deklarierten. Es hat mit den Regeln für die Wertinitialisierung zu tun.

Betrachten Sie zwei Klassen: AHat einen vom Benutzer deklarierten Konstruktor, Bhat einen vom Benutzer bereitgestellten Konstruktor:

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

Auf den ersten Blick könnte man meinen, dass sich diese beiden Konstruktoren gleich verhalten. Sehen Sie jedoch, wie sich die Wertinitialisierung anders verhält, während sich nur die Standardinitialisierung gleich verhält:

  • A a;ist die Standardinitialisierung: Das Mitglied int xist nicht initialisiert.
  • B b;ist die Standardinitialisierung: Das Mitglied int xist nicht initialisiert.
  • A a{};ist Wertinitialisierung: Das Mitglied int xist nullinitialisiert .
  • B b{};ist Wertinitialisierung: Das Mitglied int xist nicht initialisiert.

Nun sehen Sie, was passiert, wenn wir hinzufügen const:

  • const A a;ist die Standardinitialisierung: Dies ist aufgrund der in der Frage angegebenen Regel falsch .
  • const B b;ist die Standardinitialisierung: Das Mitglied int xist nicht initialisiert.
  • const A a{};ist Wertinitialisierung: Das Mitglied int xist nullinitialisiert .
  • const B b{};ist Wertinitialisierung: Das Mitglied int xist nicht initialisiert.

Ein nicht initialisierter constSkalar (z. B. das int xMitglied) wäre nutzlos: Das Schreiben darauf ist schlecht geformt (weil es ist const) und das Lesen daraus ist UB (weil es einen unbestimmten Wert enthält). Diese Regel verhindert also, dass Sie so etwas erstellen, indem Sie gezwungen werden, entweder einen Initialisierer hinzuzufügen oder sich für das gefährliche Verhalten zu entscheiden, indem Sie einen vom Benutzer bereitgestellten Konstruktor hinzufügen.

Ich denke, es wäre schön, ein Attribut [[uninitialized]]zu haben, das dem Compiler mitteilt, wenn Sie absichtlich kein Objekt initialisieren. Dann wären wir nicht gezwungen, unsere Klasse nicht trivial standardmäßig konstruierbar zu machen, um diesen Eckfall zu umgehen. Dieses Attribut wurde tatsächlich vorgeschlagen , aber genau wie alle anderen Standardattribute erfordert es kein normatives Verhalten und ist lediglich ein Hinweis für den Compiler.

Oktalist
quelle
1

Herzlichen Glückwunsch, Sie haben einen Fall erfunden, in dem es keinen benutzerdefinierten Konstruktor für die constDeklaration geben muss, ohne dass ein Initialisierer sinnvoll ist.

Können Sie jetzt eine vernünftige Neuformulierung der Regel finden, die Ihren Fall abdeckt, aber dennoch die Fälle, die illegal sein sollten, illegal macht? Ist es weniger als 5 oder 6 Absätze? Ist es einfach und offensichtlich, wie es in jeder Situation angewendet werden sollte?

Ich gehe davon aus, dass es sehr schwierig ist, eine Regel zu entwickeln, mit der die von Ihnen erstellte Deklaration Sinn ergibt, und sicherzustellen, dass die Regel auf eine Weise angewendet werden kann, die für Menschen beim Lesen von Code sinnvoll ist. Ich würde eine etwas restriktive Regel, die in den meisten Fällen das Richtige war, einer sehr nuancierten und komplexen Regel vorziehen, die schwer zu verstehen und anzuwenden war.

Die Frage ist, gibt es einen zwingenden Grund, warum die Regel komplexer sein sollte? Gibt es einen Code, der sonst sehr schwer zu schreiben oder zu verstehen wäre und der viel einfacher geschrieben werden kann, wenn die Regel komplexer ist?

Allgegenwärtig
quelle
1
Hier ist mein vorgeschlagener Wortlaut: "Wenn ein Programm die Standardinitialisierung eines Objekts eines const-qualifizierten Typs T anfordert, muss T ein Nicht-POD-Klassentyp sein." Dies wäre const POD x;genauso illegal wie const int x;illegal (was sinnvoll ist, weil dies für einen POD nutzlos ist), aber const NonPOD x;legal (was sinnvoll ist, weil es Unterobjekte enthalten könnte, die nützliche Konstruktoren / Destruktoren enthalten, oder einen nützlichen Konstruktor / Destruktor selbst haben könnte). .
Karu
@ Karu - Diese Formulierung könnte funktionieren. Ich bin an RFC-Standards gewöhnt und habe daher das Gefühl, dass "T soll sein" "T muss sein" lauten sollte. Aber ja, das könnte funktionieren.
Omnifarious
@Karu - Was ist mit struct NonPod {int i; virtuelle Leere f () {}}? Es ist nicht sinnvoll, const NonPod x zu erstellen. legal.
Gruzovator
1
@gruzovator Wäre es sinnvoller, wenn Sie einen leeren benutzerdefinierten Standardkonstruktor hätten? Mein Vorschlag versucht nur, eine sinnlose Anforderung des Standards zu entfernen; mit oder ohne gibt es immer noch unendlich viele Möglichkeiten, Code zu schreiben, der keinen Sinn ergibt.
Karu
1
@ Karu Ich stimme dir zu. Aufgrund dieser Regel im Standard gibt es viele Klassen, für die ein benutzerdefinierter leerer Konstruktor erforderlich ist . Ich mag gcc Verhalten. Es erlaubt zB struct NonPod { std::string s; }; const NonPod x;und gibt einen Fehler, wenn NonPod iststruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator