Wann ist ein privater Konstruktor kein privater Konstruktor?

88

Angenommen, ich habe einen Typ und möchte seinen Standardkonstruktor privat machen. Ich schreibe folgendes:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

Toll.

Aber dann stellt sich heraus, dass der Konstruktor nicht so privat ist, wie ich dachte:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

Dies erscheint mir als sehr überraschendes, unerwartetes und explizit unerwünschtes Verhalten. Warum ist das in Ordnung?

Barry
quelle
24
Ist die C c{};aggregierte Initialisierung nicht so, dass kein Konstruktor aufgerufen wird?
NathanOliver
5
Was @NathanOliver gesagt hat. Sie haben keinen vom Benutzer bereitgestellten Konstruktor, also Cein Aggregat.
Kerrek SB
5
@KerrekSB Gleichzeitig war es für mich ziemlich überraschend, dass der Benutzer, der explizit einen Ctor deklariert, diesen Ctor nicht vom Benutzer bereitstellt.
Angew ist nicht mehr stolz auf SO
1
@Angew Deshalb sind wir alle hier :)
Barry
2
@Angew Wenn es ein öffentlicher =defaultCtor wäre, würde das vernünftiger erscheinen. Aber der private =defaultCtor scheint eine wichtige Sache zu sein, die nicht ignoriert werden sollte. Was mehr ist , class C { C(); } inline C::C()=default;ganz anders zu sein , ist etwas überraschend.
Yakk - Adam Nevraumont

Antworten:

58

Der Trick ist in C ++ 14 8.4.2 / 5 [dcl.fct.def.default]:

... Eine Funktion wird vom Benutzer bereitgestellt, wenn sie vom Benutzer deklariert und bei ihrer ersten Deklaration nicht explizit als Standard festgelegt oder gelöscht wurde. ...

Dies bedeutet, dass Cder Standardkonstruktor des Benutzers nicht vom Benutzer bereitgestellt wird, da er bei seiner ersten Deklaration explizit als Standard festgelegt wurde. CHat als solches keine vom Benutzer bereitgestellten Konstruktoren und ist daher ein Aggregat gemäß 8.5.1 / 1 [dcl.init.aggr]:

Ein Aggregat ist ein Array oder eine Klasse (Abschnitt 9) ohne vom Benutzer bereitgestellte Konstruktoren (12.1), ohne private oder geschützte nicht statische Datenelemente (Abschnitt 11), ohne Basisklassen (Abschnitt 10) und ohne virtuelle Funktionen (10.3) ).

Angew ist nicht mehr stolz auf SO
quelle
13
Tatsächlich wird ein kleiner Standardfehler: Die Tatsache, dass der Standard-Ctor privat war, wird in diesem Zusammenhang praktisch ignoriert.
Yakk - Adam Nevraumont
2
@ Yakk Ich fühle mich nicht qualifiziert, das zu beurteilen. Die Formulierung, dass der Ctor nicht vom Benutzer bereitgestellt wird, sieht jedoch sehr bewusst aus.
Angew ist nicht mehr stolz auf SO
1
@ Yakk: Nun ja und nein. Wenn die Klasse Datenelemente hätte, hätten Sie die Möglichkeit, diese privat zu machen. Ohne Datenmitglieder gibt es nur sehr wenige Situationen, in denen diese Situation jemanden ernsthaft betreffen würde.
Kerrek SB
2
@KerrekSB Es ist wichtig, wenn Sie versuchen, die Klasse als eine Art "Zugriffstoken" zu verwenden, z. B. um zu steuern, wer eine Funktion aufrufen kann, basierend darauf, wer ein Objekt der Klasse erstellen kann.
Angew ist nicht mehr stolz auf SO
5
@Yakk Noch interessanter ist, dass es C{}funktioniert, auch wenn der Konstruktor deleted ist.
Barry
55

Sie rufen nicht den Standardkonstruktor auf, sondern verwenden die Aggregatinitialisierung für einen Aggregattyp. Aggregattypen dürfen einen voreingestellten Konstruktor haben, sofern dieser standardmäßig dort ist, wo er zuerst deklariert wurde:

Aus [dcl.init.aggr] / 1 :

Ein Aggregat ist ein Array oder eine Klasse (Klausel [Klasse]) mit

  • Keine vom Benutzer bereitgestellten Konstruktoren ([class.ctor]) (einschließlich der von einer Basisklasse geerbten ([namespace.udecl])),
  • keine privaten oder geschützten nicht statischen Datenelemente (Klausel [class.access]),
  • keine virtuellen Funktionen ([class.virtual]) und
  • Keine virtuellen, privaten oder geschützten Basisklassen ([class.mi]).

und aus [dcl.fct.def.default] / 5

Explizit voreingestellte Funktionen und implizit deklarierte Funktionen werden gemeinsam als voreingestellte Funktionen bezeichnet, und die Implementierung muss implizite Definitionen für sie bereitstellen ([class.ctor] [class.dtor], [class.copy]), was bedeuten kann, dass sie als gelöscht definiert werden . Eine Funktion wird vom Benutzer bereitgestellt, wenn sie vom Benutzer deklariert und bei ihrer ersten Deklaration nicht explizit als Standard festgelegt oder gelöscht wurde. Eine vom Benutzer bereitgestellte explizit voreingestellte Funktion (dh explizit voreingestellte Funktion nach ihrer ersten Deklaration) wird an dem Punkt definiert, an dem sie explizit voreingestellt ist. Wenn eine solche Funktion implizit als gelöscht definiert ist, ist das Programm fehlerhaft.[Hinweis: Das Deklarieren einer Funktion als Standard nach ihrer ersten Deklaration kann eine effiziente Ausführung und präzise Definition ermöglichen und gleichzeitig eine stabile binäre Schnittstelle zu einer sich entwickelnden Codebasis ermöglichen. - Endnote]

Daher sind unsere Anforderungen an ein Aggregat:

  • keine nicht öffentlichen Mitglieder
  • keine virtuellen Funktionen
  • Keine virtuellen oder nicht öffentlichen Basisklassen
  • Keine vom Benutzer bereitgestellten Konstruktoren geerbt oder anderweitig. Dies erlaubt nur Konstruktoren, die:
    • implizit deklariert, oder
    • explizit deklariert und gleichzeitig als voreingestellt definiert.

C erfüllt alle diese Anforderungen.

Natürlich können Sie dieses falsche Standardkonstruktionsverhalten beseitigen, indem Sie einfach einen leeren Standardkonstruktor angeben oder den Konstruktor nach der Deklaration als Standard definieren:

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
jaggedSpire
quelle
2
Ich mag diese Antwort etwas besser als die von Angew, aber ich denke, sie würde von einer Zusammenfassung am Anfang in höchstens zwei Sätzen profitieren.
PJTraill
7

Die Antworten von Angew und jaggedSpire sind ausgezeichnet und gelten für. Und. Und.

In Die Dinge ändern sich ein wenig und das Beispiel im OP wird nicht mehr kompiliert:

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

Wie aus den beiden Antworten hervorgeht, funktionieren die beiden letztgenannten Deklarationen, weil Ces sich um ein Aggregat handelt und es sich um eine Aggregatinitialisierung handelt. Aufgrund von P1008 (anhand eines motivierenden Beispiels, das sich nicht allzu sehr vom OP unterscheidet) ändert sich die Definition der aggregierten Änderungen in C ++ 20 von [dcl.init.aggr] / 1 :

Ein Aggregat ist ein Array oder eine Klasse ([Klasse]) mit

  • keine vom Benutzer deklarierten oder geerbten Konstruktoren ([class.ctor]),
  • keine privaten oder geschützten direkten nicht statischen Datenelemente ([class.access]),
  • keine virtuellen Funktionen ([class.virtual]) und
  • Keine virtuellen, privaten oder geschützten Basisklassen ([class.mi]).

Hervorhebung von mir. Jetzt sind keine vom Benutzer deklarierten Konstruktoren mehr erforderlich , während es früher (wie beide Benutzer in ihren Antworten zitieren und historisch für C ++ 11 , C ++ 14 und C ++ 17 angezeigt werden können ) keine vom Benutzer bereitgestellten Konstruktoren war . Der Standardkonstruktor für Cist vom Benutzer deklariert, aber nicht vom Benutzer bereitgestellt und ist daher in C ++ 20 kein Aggregat mehr.


Hier ist ein weiteres anschauliches Beispiel für aggregierte Änderungen:

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

Bwar kein Aggregat in C ++ 11 oder C ++ 14, da es eine Basisklasse hat. Als Ergebnis B{}ruft nur den Standard - Konstruktor ( vom Benutzer deklariert , aber nicht vom Benutzer zur Verfügung gestellt), der Zugriff hat A‚s geschützten Standardkonstruktor.

In C ++ 17 wurden als Ergebnis von P0017 Aggregate erweitert, um Basisklassen zu ermöglichen. Bist ein Aggregat in C ++ 17, dh es B{}handelt sich um eine Aggregatinitialisierung, bei der alle Unterobjekte - einschließlich des AUnterobjekts - initialisiert werden müssen . Da Ader Standardkonstruktor jedoch geschützt ist, haben wir keinen Zugriff darauf, sodass diese Initialisierung fehlerhaft ist.

In C ++ 20 ist der vom BBenutzer deklarierte Konstruktor aufgrund des vom Benutzer deklarierten Konstruktors wieder kein Aggregat B{}mehr. Daher wird wieder der Standardkonstruktor aufgerufen, und dies ist wiederum eine wohlgeformte Initialisierung.

Barry
quelle