In C++98 kregen lidfuncties toegang tot het impliciete object via een verborgen this pointer, wat vereiste dat er verschillende overloads nodig waren om const en niet-const contexten te behandelen, terwijl C++11 ref-gekwalificeerde overloads introduceerde om lvalue- en rvalue-objecten te onderscheiden. Dit vereiste mogelijk vier overloads per functie om alle cv-ref combinaties te dekken, wat zorgde voor aanzienlijke code-dubbelingen en onderhoudsproblemen voor generieke bibliotheken.
Het kernprobleem ontstaat wanneer een lidfunctie het object met dezelfde waarde categorie en cv-kwalificatie moet retourneren als de oproeper om efficiënte move-semantiek mogelijk te maken of om dangling references te voorkomen. Zonder de type van het object te deduceren, schreven ontwikkelaars uitgebreide overloadsets of compromitteerden ze de kopie-semantic, wat leidde tot inefficiënte rvalue-behandeling of subtiele lifetime-bugs in generieke code die objectreferenties overbracht.
C++23 introduceert expliciete objectparameters, die de syntaxis void foo(this auto&& self) mogelijk maken. Hier wordt self een afgeleide parameter die de waarde categorie en cv-kwalificaties van het object vastlegt, wat de behoefte aan aparte & en && overloads elimineert, aangezien std::forward<decltype(self)>(self) de juiste categorie doorgeeft. Statische lidfuncties hebben echter geen impliciet object, dus de toepassing van deze syntaxis daarop schendt de fundamentele eis om een object te hebben om aan self te binden, waardoor het programma volgens de standaard ongeldig is.
// Voor C++23: Vier overloads nodig 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: Enkele overload class Builder { public: template<typename Self> auto setName(this Self&& self, ...) -> Self&& { // ... return std::forward<Self>(self); } };
Ons team ontwikkelde een hoogwaardige JSON bibliotheek waarin DOM knooppunten methode-ketens ondersteunden voor boomconstructie, waarbij de Node klasse addChild() methoden moest bieden met verschillende returnsemantiek. Deze methoden moesten de ouder teruggeven per referentie wanneer de ouder een lvalue was om verdere mutatie mogelijk te maken, maar per waarde wanneer de ouder een rvalue-tijdelijke was om move-elision mogelijk te maken en onbedoelde wijziging van vervallende objecten te voorkomen.
De initiële implementatie gebruikte traditionele ref-gekwalificeerde overloads. We onderhielden vier versies van addChild: een teruggeef van Node& voor lvalues, een teruggeef van Node const& voor const lvalues, een teruggeef van Node&& voor rvalues, en een teruggeef van Node const&& voor const rvalues. Deze benadering voldeed aan de prestatie-eisen maar verviervoudigde ons testoppervlak, en een kritieke bug deed zich voor waar de const&& overload ten onrechte een dangling referentie teruggeef vanwege een kopieer-plakfout uit de & overload.
We overwoogen om ref-kwalificaties volledig op te geven en altijd per waarde terug te geven, vertrouwend op RVO om kopieën te optimaliseren, maar dit dwong onnodige verplaatsingen af op benoemde objecten en verstoorde API-compatibiliteit met bestaande code die verwijzingen naar het teruggegeven knooppunt opsloeg. We beoordeelden ook CRTP met een basisklas sjabloon die het afgeleide type deduceerde, maar dit exposeerde implementatiedetails aan gebruikers en compliceerde erfgoedhiërarchieën zonder het probleem van waarde categorie propagatie volledig op te lossen.
Het aannemen van de expliciete objectparameters in C++23 stelde ons in staat om de overloadset samen te voegen in een enkele sjabloonmethode: template<typename Self> auto addChild(this Self&& self, ...) -> Self. Dit ving de exacte waarde categorie die nodig was, maakte perfecte forwarding mogelijk zonder redundantie van std::move of std::forward in de implementatie, en verminderde de cyclomatische complexiteit van de methode tot één pad. Het resultaat was een vermindering van 75% in boilerplatecode en eliminatie van de categorie bugs met betrekking tot overload-divergentie.
Waarom voorkomt het gebruik van expliciete objectparametersyntax dat de functie traditionele cv-kwalificaties of ref-kwalificaties na de parameterlijst krijgt?
Traditionele lidfuncties plaatsen cv-kwalificaties en ref-kwalificaties na de parameterlijst om het type van de impliciete this pointer aan te passen. Met expliciete objectparameters codeert this Self&& self al de cv-kwalificatie en referentiecategorie binnen de type-afleiding van Self. Het toevoegen van extra kwalificaties zoals const of & na de parameterlijst zou proberen een niet-bestaand impliciet object te kwalificeren, wat een contradictie in het type systeem creëert. De standaard verbiedt deze combinatie expliciet omdat de expliciete parameter de rol van zowel de parameter als de kwalificaties overneemt, en het toestaan van beide zou ambiguïteit creëren over welke semantiek de oproep bepaalt.
Hoe verschilt naamlookup binnen de functie-lichaam wanneer expliciete objectparameters worden gebruikt versus traditionele lidfuncties?
In traditionele lidfuncties zoekt ongekwalificeerde naamlookup automatisch in de klasscope alsof this-> werd voorafgegaan. Bij expliciete objectparameters is er geen impliciete this pointer; de parameter self moet expliciet worden gebruikt om toegang te krijgen tot leden. Kandidaten nemen vaak aan dat member binnen void foo(this auto& self) automatisch resolveert naar this->member, maar het vereist eigenlijk self. kwalificatie of expliciete klaskwalificatie zoals ClassName::member. Dit verandert de fundamentele lookup-regels en vereist aanpassing bij het migreren van code, met name voor het verkrijgen van toegang tot beschermde leden vanuit afgeleide klassen waarbij self. de toegang controleert tegen het afgeleide type in plaats van het statische klassetype.
Kunnen expliciete objectparameters deelnemen aan virtuele functie-overerving, en welke beperkingen gelden voor de overwijdingsrelatie?
Expliciete objectparameters kunnen verschijnen in virtuele functies, maar ze veranderen fundamenteel de regels voor overeenstemming van overwijzing. Een basisklasse die virtual void bar(this Base& self) declareert, kan niet worden overschreven door een afgeleide klasse die void bar(this Derived& self) declareert, hoewel traditionele overrides covariânt retourtypen toestaan. De expliciete objectparameter maakt deel uit van de handtekening van de functie voor overeenstemmingsdoeleinden van de overwijzing. Aangezien Base& en Derived& verschillende types zijn, vormt dit geen geldige overwijzing. Dit voorkomt het gangbare patroon van het gebruik van expliciete objectparameters om "sfinae-vriendelijke" virtuele functies of type-behoudende methode-ketens in polymorfe hiërarchieën te bereiken. Om te overschrijden, moet de afgeleide functie exact overeenkomen met het expliciete parameter type van de basis, waardoor de voordelen van deductie voor die parameter in de context van overwijzing worden opgeheven.