Im C++98 haben Mitgliedsfunktionen das implizite Objekt über einen versteckten this-Zeiger angesprochen, was unterschiedliche Überladungen erforderte, um mit const und non-const Kontexten umzugehen, während C++11 Ref-Qualifizierer einführte, um lvalue- und rvalue-Objekte zu unterscheiden. Dies erforderte potenziell vier Überladungen pro Funktion, um alle cv-ref Kombinationen abzudecken, was zu erheblichem Code-Duplikat und Wartungsaufwand für generische Bibliotheken führte.
Das Kernproblem besteht darin, dass eine Mitgliedsfunktion das Objekt mit derselben Wertkategorie und cv-Qualifikation wie der Aufrufer zurückgeben muss, um effiziente Move-Semantiken zu ermöglichen oder hängende Referenzen zu verhindern. Ohne die Ableitung des Objekttyps schrieben Entwickler ausführliche Überladungssets oder gingen auf Kopiersemantiken ein, was zu ineffizienter Handhabung von rvalues oder subtilen Lebensdauerfehlern im generischen Code führte, die Objektverweise propagierten.
C++23 führt explizite Objektparameter ein, die die Syntax void foo(this auto&& self) ermöglichen. Hier wird self zu einem abgeleiteten Parameter, der die Wertkategorie und die cv-Qualifikationen des Objekts erfasst, und die Notwendigkeit separater & und && Überladungen wird beseitigt, da std::forward<decltype(self)>(self) die korrekte Kategorie propagiert. Statische Mitgliedsfunktionen hingegen haben überhaupt kein implizites Objekt, weshalb die Anwendung dieser Syntax auf sie die grundlegende Anforderung verletzt, ein Objekt zu haben, an das self gebunden werden kann, was das Programm gemäß dem Standard ungültig macht.
// Vor C++23: Vier Überladungen nötig class Builder { public: Builder& setName(...) & { /* ... */ return *this; } Builder const& setName(...) const& { /* ... */ return *this; } Builder&& setName(...) && { /* ... */ return std::move(*this); } Builder const&& setName(...) const&& { /* ... */ return std::move(*this); } }; // C++23: Einzelne Überladung class Builder { public: template<typename Self> auto setName(this Self&& self, ...) -> Self&& { // ... return std::forward<Self>(self); } };
Unser Team entwickelte eine leistungsstarke JSON-Bibliothek, in der DOM-Knoten die Methodenverkettung für den Baumaufbau unterstützten, was erforderte, dass die Node-Klasse addChild()-Methoden mit unterschiedlichen Rückgabesemantiken bereitstellt. Diese Methoden mussten den Elternknoten durch Referenz zurückgeben, wenn der Elternknoten ein lvalue war, um weitere Mutationen zu ermöglichen, aber durch Wert, wenn der Elternknoten ein rvalue-Temporäres war, um Move-Älision zu ermöglichen und eine versehentliche Modifikation von ablaufenden Objekten zu verhindern.
Die ursprüngliche Implementierung verwendete traditionelle referenzqualifizierte Überladungen. Wir hatten vier Versionen von addChild: eine, die Node& für lvalues zurückgibt, eine, die Node const& für const lvalues zurückgibt, eine, die Node&& für rvalues zurückgibt, und eine, die Node const&& für const rvalues zurückgibt. Dieser Ansatz erfüllte die Leistungsanforderungen, vervierfachte jedoch unseren Testumfang, und ein kritischer Fehler trat auf, bei dem die const&& Überladung versehentlich eine hängende Referenz zurückgab, aufgrund eines Copy-Paste-Fehlers von der & Überladung.
Wir erwogen, ganz auf Ref-Qualifizierer zu verzichten und immer durch Wert zurückzugeben, indem wir auf RVO setzten, um Kopien zu optimieren, jedoch führten dies zu unnötigen Moves bei benannten Objekten und brach die API-Kompatibilität mit bestehendem Code, der Referenzen auf den zurückgegebenen Knoten speicherte. Wir bewerteten auch CRTP mit einer Basisklassentemplate, die den abgeleiteten Typ abgeleitet hat, aber dies öffnete Implementierungsdetails für Benutzer und komplizierte Vererbungshierarchien, während es das Problem der Wertkategorien-Propagation nicht vollständig löste.
Die Einführung von C++23-expliziten Objektparametern ermöglichte es uns, das Überladungsset in eine einzige Template-Methode zu konsolidieren: template<typename Self> auto addChild(this Self&& self, ...) -> Self. Dies erfasste die genau benötigte Wertkategorie, ermöglichte perfekte Übergaben ohne std::move oder std::forward Redundanz in der Implementierung und reduzierte die zyklomatische Komplexität der Methode auf einen Pfad. Das Ergebnis war eine 75%ige Reduktion des Boilerplate-Codes und die Beseitigung der Kategorie von Fehlern im Zusammenhang mit Überladungsabweichung.
Warum verhindert die Verwendung der Syntax für explizite Objektparameter, dass der Funktion traditionelle cv-Qualifizierer oder Ref-Qualifizierer nach der Parameterliste angehängt werden?
Traditionelle Mitgliedsfunktionen platzieren cv-Qualifizierer und Ref-Qualifizierer nach der Parameterliste, um den Typ des impliziten this-Zeigers zu modifizieren. Bei expliziten Objektparametern kodiert this Self&& self bereits die cv-Qualifikation und die Referenzkategorie innerhalb der Typableitung von Self. Das Hinzufügen zusätzlicher Qualifizierer wie const oder & nach der Parameterliste würde versuchen, ein nicht vorhandenes implizites Objekt zu qualifizieren, was zu einem Widerspruch im Typsystem führt. Der Standard verbietet ausdrücklich diese Kombination, da der explizite Parameter die Rolle sowohl des Parameters als auch der Qualifizierer übernimmt, und das Zulassen beider würde Mehrdeutigkeit darüber schaffen, welche Semantiken den Aufruf steuern.
Wie unterscheidet sich die Namenssuche innerhalb der Funktion, wenn explizite Objektparameter im Vergleich zu traditionellen Mitgliedsfunktionen verwendet werden?
In traditionellen Mitgliedsfunktionen sucht die unqualifizierte Namenssuche automatisch im Klassenscope, als ob this-> vorangestellt wäre. Bei expliziten Objektparametern gibt es keinen impliziten this-Zeiger; der Parameter self muss explizit verwendet werden, um auf Mitglieder zuzugreifen. Kandidaten gehen oft davon aus, dass member innerhalb von void foo(this auto& self) automatisch zu this->member aufgelöst wird, tatsächlich ist jedoch eine self.-Qualifikation oder explizite Klassenqualifikation wie ClassName::member erforderlich. Dies ändert die grundlegenden Regeln für die Namenssuche und erfordert eine Anpassung beim Migrieren von Code, insbesondere zum Zugriff auf geschützte Mitglieder aus abgeleiteten Klassen, wo self. explizit die Zugriffsprüfung gegen den abgeleiteten Typ auslöst, anstelle des statischen Klassentyps.
Können explizite Objektparameter an virtuellen Funktionsüberladungen teilnehmen, und welche Einschränkungen gelten für die Überladebeziehung?
Explizite Objektparameter können in virtuellen Funktionen erscheinen, aber sie ändern fundamental die Regeln für das Überladungs-Matching. Eine Basisklasse, die virtual void bar(this Base& self) deklariert, kann nicht von einer abgeleiteten Klasse, die void bar(this Derived& self) deklariert, überschrieben werden, obwohl traditionelle Überladungen konvergente Rückgabetypen erlauben. Der explizite Objektparameter wird Teil der Signatur der Funktion für die Überladungs-Matching-Zwecke. Da Base& und Derived& unterschiedliche Typen sind, stellt dies keine gültige Überladung dar. Dies verhindert das gängige Muster, explizite Objektparameter zu verwenden, um "sfinae-freundliche" virtuelle Funktionen oder typbewahrende Methodenverkettungen in polymorphen Hierarchien zu erreichen. Um zu überschreiben, muss die abgeleitete Funktion den expliziten Parametertyp der Basis genau übereinstimmen, was die Ableitungs vorteile für diesen Parameter im Überlandungskontext negiert.