A C++20-as örökség egyik leggyakoribb tervezési terhe a tagfüggvények
objektumhoz való viszonyának sokféle kódolása. Egy egyszerű getterhez
négy változat kell, ha meg akarjuk őrizni a const-ságot és
az értékkategóriát; a statikus polimorfizmus CRTP-vel gyakran
static_cast-olással és sablonparaméterként átadott derived
típussal jár; a rekurzív lambda pedig C++20-ban csak körülményesen,
std::function-nel vagy külső segédstruktúrával írható meg.
A C++23 explicit object parameter — közismertebb nevén deducing
this — ezekre ad nyelvi szintű, egységes eszközt. Nem új futásidejű
mechanizmus: a tagfüggvény objektumparaméterét teszi láthatóvá és
dedukálhatóvá. A mostani rész ezt a nyelvi eszközt tárgyalja; a C++23
laborprofil és a feature-testek általános kezelése a C++23 laborprofil, szabványstátusz és feature-test
részben már rögzített alapra épül.
A klasszikus C++20-as overload-négyes tipikus példája egy konténer eleméhez való hozzáférés:
struct Param {
int value;
int& get() & { return value; }
const int& get() const& { return value; }
int&& get() && { return std::move(value); }
const int&& get() const&& { return std::move(value); }
};Négy deklaráció, négy visszatérési típus, és a négy változat közötti különbség csak a függvény minősítőiben van. Ugyanez a CRTP-ben:
template<class D>
struct Shape {
double area() const { return static_cast<const D*>(this)->area_impl(); }
};
struct Circle : Shape<Circle> {
double r{1.0};
double area_impl() const { return 3.141592653589793 * r * r; }
};A Shape csak a D sablonparaméteren
keresztül tudja, hogy valójában Circle-ról van szó; a
static_cast a derived típusra mutat. A rekurzív lambda
C++20-ban jellemzően így néz ki:
#include <functional>
std::function<int(int)> fact = [&](int n) {
return n <= 1 ? 1 : n * fact(n - 1);
};Ez működik, de típus-erasure-t és indirekciót hoz be, ráadásul a
fact élettartamához kötött. Az explicit object parameter
mindhárom mintát egyszerűsíti, de nem mindenható: a virtuális dispatch,
a statikus tagfüggvény, a konstruktor/destruktor és a hagyományos
tagfüggvény-pointer szemantikája továbbra is határt szab.
Az explicit object parameter olyan
tagfüggvény-paraméter, amelyet a this specifier előz meg,
és amely a paraméterlistában az első helyen áll. A függvény tagfüggvény
marad, de nincs implicit this paramétere: az objektumot a
hívás az első, explicit paraméteren keresztül adja át. A deducing
this elnevezés arra utal, hogy ennek a paraméternek a típusát —
jellemzően egy Self sablonparamétert — a hívás
objektumargumentumából dedukáljuk.
A leggyakoribb alak:
struct Widget {
template<class Self>
void f(this Self&& self) {
// self a hívó objektum; a this kulcsszó nem használható
}
};A this auto&& self ugyanezt jelenti egy
implicit, a fordító által bevezetett sablonparaméterrel. A paraméter
lehet érték szerinti is:
struct Point {
int x, y;
int sum(this Point self) { return self.x + self.y; }
};Ekkor a hívás objektumból másolat készül; a függvény a saját
példányát módosítja, az eredetit nem. Kis, olcsón másolható típusoknál
ez kényelmes, nagy típusnál viszont kerülendő. A
this Self& self csak lvalue objektumot fogad el, a
this Self const& self pedig const hozzáférést ad; a
teljes értékkategória- és const-propagáláshoz a
this Self&& self a természetes választás.
A fontos feltételek:
virtual, nem lehet static, és
nem lehet konstruktor vagy destruktor.const, volatile,
& vagy && minősítő; ezeket a
Self típusa hordozza. A
void f(this Self&& self) const alak ezért
hibás.this; a
this kulcsszó nem használható, helyette a paraméter nevével
hivatkozunk az objektumra.operator()-ában is megengedett; ez teszi
lehetővé a rekurzív lambdát.A this Self&& self paraméter dedukciója ugyanaz,
mint egy forwarding reference-é. Ha a hívó objektum típusa
C:
| Hívás objektuma | Self |
self deklarált típusa |
|---|---|---|
C lvalue |
C& |
C& |
const C lvalue |
const C& |
const C& |
C rvalue |
C |
C&& |
const C rvalue |
const C |
const C&& |
A referencia-összevonás adja a második oszlopból a harmadikat:
Self = C& esetén C& && →
C&; Self = C esetén
C&&. A const a Self
típusába kerül, nem a függvényre. Fontos következmény, hogy a függvényen
belül a self név mindig lvalue kifejezés, akármilyen
értékkategóriával érkezett az objektum; ezért kell a továbbításhoz
std::forward<Self>(self) vagy a tagokra
std::forward_like<Self>(...).
forward_like-kalA négyes egyetlen függvénysablonnal kiváltható:
#include <cstddef>
#include <type_traits>
#include <utility>
#include <vector>
struct Buffer {
std::vector<int> data;
template<class Self>
auto&& at(this Self&& self, std::size_t i) {
return std::forward_like<Self>(self.data[i]);
}
};A std::forward_like<T>(x) a T-ből
származtatja azt a const- és értékkategória-információt, amelyet az
x-re alkalmazni kell. Itt Self hordozza az
objektum const-ságát és értékkategóriáját, miközben
self.data[i] önmagában lvalue int kifejezés. A
forward_like ezt alakítja int&,
const int&, int&& vagy
const int&& típusúvá. A
std::forward<Self>(self).data[i] ezzel szemben nem
elegendő: a std::vector::operator[] nincs ref-minősítve,
ezért rvalue vektoron is lvalue referenciát ad vissza; a value category
nem terjed át a subscripten. Ez tipikus diagnosztikai hiba, amikor
valaki a négyesből csak a const/non-const párt teszteli, az rvalue eset
pedig csendben lvalue referenciát ad.
A teljes példa fordítása a laborban rögzített fordítókiadással:
g++ -std=c++23 -Wall -Wextra -pedantic-errors -O2 deducing_this.cpp -o deducing_this
clang++ -std=c++23 -Wall -Wextra -pedantic-errors -O2 deducing_this.cpp -o deducing_thisA támogatás tényét nem feltételezzük; feature-testtel és compile probe-bal ellenőrizzük:
#if !defined(__cpp_explicit_this_parameter) || __cpp_explicit_this_parameter < 202110L
#error "A forditonak C++23 explicit object parametert kell tamogatnia."
#endif
#if !defined(__cpp_lib_forward_like) || __cpp_lib_forward_like < 202207L
#error "A standard konyvtarnak std::forward_like-ot kell nyujtania."
#endifA nyelvi és a könyvtári támogatás külön ellenőrizendő: a
__cpp_explicit_this_parameter a nyelvi képességet, a
__cpp_lib_forward_like a könyvtári eszközt jelzi. Ha az
utóbbi hiányzik, C++20-as fallbackként a négy overload, vagy kézi
static_cast-olás a Self alapján használható; a
std::forward önmagában nem elég a fenti
operator[]-probléma miatt.
Élettartam szempontjából a forward_like nem varázslat:
ha a getter auto&&-t ad vissza, és az objektum
temporális, a visszaadott referencia a temporális tagjára mutat, amely a
teljes kifejezés végén megszűnik. Ez ugyanaz a dangling-helyzet, mint a
ref-minősített gettereknél; a részletes értékkategória- és
temporary-szabályokat a Temporary, value category és
dangling rész tárgyalta. Ha a hívó nem tudja garantálni az owner
élettartamát, adjunk vissza értéket, ne referenciát.
A CRTP-s Shape/Circle példa explicit object
parameterrel így egyszerűsödik:
struct Shape {
template<class Self>
double area(this Self const& self) {
static_assert(requires { self.area_impl(); },
"A Self tipusnak area_impl() tagfuggvenyt kell adnia");
return self.area_impl();
}
};
struct Circle : Shape {
double r{1.0};
double area_impl() const { return 3.141592653589793 * r * r; }
};Eltűnt a D sablonparaméter, a static_cast,
és a derived osztálynak nem kell önmagát átadnia a base-nek. A
Self a hívás statikus típusából dedukálódik, így a
Circle c; c.area(); hívásban
Self = const Circle& (a const&
paraméter miatt), és a self.area_impl() megtalálható. A
requires-szel vagy static_assert-tel adott
követelmény azonban csak a függvény hívásakor, az adott
Self-re ellenőrződik, nem a Circle osztály
definiálásakor.
Ez a fő különbség a CRTP-hez képest. A CRTP base már a
Shape<Circle> példányosításakor ismeri a
Circle-t, ezért ott class-scope-ban is lehet
static_assert-et, typedefet vagy egyéb, a derived típusra
épülő deklarációt elhelyezni. A deducing this nem tud tagokat
„injektálni” a derived osztályba, és nem tud a base-ben class-scope-ban
a derived típusra hivatkozni. Ha a statikus polimorfizmus részeként a
base referencia mögül is a derived függvényét akarjuk hívni, a deducing
this nem elegendő:
Circle c;
Shape const& s = c;
// s.area(); // Self = Shape, a Shape-nek nincs area_impl() tagjaA CRTP-s Shape<Circle> const& ezzel szemben
tudja a Circle-t. A deducing this tehát kiváltja a CRTP
gyakori static_cast-os mintáját, de nem váltja ki a CRTP
teljes statikus polimorfizmusát. Marad továbbá a virtuális dispatch
hiánya: nincs vtable, nincs runtime polymorphic interfész, és a
Self mindig a kifejezés statikus típusa szerint
dedukálódik. A CRTP és a policy design korábbi tárgyalása a Customization point, CRTP és policy design részben
található.
A lambda operator()-a is tagfüggvény, ezért kaphat
explicit object parametert. Ezzel a closure saját magát névvel érheti
el:
#include <cassert>
int main() {
auto fact = [](this auto&& self, int n) -> int {
return n <= 1 ? 1 : n * self(n - 1);
};
assert(fact(5) == 120);
}A fact(5) hívásban a fact closure az
explicit object paraméter, az 5 pedig az n. A
self a closure objektumra kötődik, így
self(n - 1) újra a lambda operator()-át hívja.
Nem kell std::function, nem kell a lambdát capture-rel
önmagára hivatkoztatni, és a rekurzió típusa a closure típusa marad. A
this auto self érték szerinti változat is működik, de a
closure-t minden rekurzív hívásnál másolja; a
this auto&& ezt elkerüli. A closure élettartamára
ugyanazok a szabályok vonatkoznak, mint bármely lambdára: ha a closure-t
visszaadjuk vagy eltároljuk, a capture-ök élettartamát továbbra is
biztosítani kell; erről a Lambda, capture és closure
lifetime rész szól.
| Kérdés | Explicit object parameterrel |
|---|---|
virtual tagfüggvény |
nem megengedett |
static tagfüggvény |
nem megengedett |
| konstruktor/destruktor | nem megengedett |
const/volatile/&/&&
a függvényen |
nem írható; a Self hordozza |
implicit this |
nincs; a this kulcsszó nem használható |
lambda operator() |
megengedett |
érték szerinti self |
megengedett, másol/mozgat; nagy típusnál kerülendő |
tagfüggvény-pointer (&C::f) |
a szabvány külön szabályozza; ABI-érzékeny, nem tekintendő stabil interfésznek |
| C++20-as fallback | négy overload, CRTP, std::function/Y-combinator |
A tagfüggvény-pointer kérdése külön óvatosságot érdemel. Az explicit
object parameteres függvény címe és hívása nem feltétlenül követi a
hagyományos, implicit this-es tagfüggvény-pointer
szemantikáját; ezt a konkrét fordítóval és a szabvány vonatkozó
szakaszával kell ellenőrizni. Shared library- vagy FFI-határon különösen
nem tanácsos ilyen függvény címét stabil ABI-elemként exportálni. A C
interfészhez továbbra is extern "C"-s szabad függvény vagy
opaque handle való; erről a C++ ABI, C interfész és
FFI rész szól.
Mikor nem alkalmazzuk? Ha a kódnak C++20-on is fordulnia kell; ha
valódi virtuális interfészre van szükség; ha a CRTP class-scope-s
követelményeit vagy taginjekcióját akarjuk; ha a stabil bináris ABI a
cél; vagy ha a meglévő kódban az implicit this-re épülő
makrók, tagpointerek vannak. Az explicit object parameter erős eszköz,
de nem helyettesíti a virtuális polimorfizmust és nem tesz hordozhatóvá
egy ABI-határt.
const a paraméterlista után:
void f(this Self&& self) const hibás. A constot a
Self-ben kell kifejezni, például
this Self const& self vagy
this Self&& self mellett a hívó const
objektummal.this használata a törzsben: nincs
implicit this; a paraméter nevével kell hivatkozni.std::move(self): mindig rvalue-re
kényszerít, és const objektumnál nem is működik úgy, ahogy várnánk.
Helyette std::forward<Self>(self) vagy tagokra
std::forward_like<Self>(...).std::forward<Self>(self).data[i] a négyes
kiváltására: a vector::operator[] nem
ref-minősített, ezért rvalue objektumon is lvalue referenciát ad; a
value category nem terjed át. Ilyenkor forward_like
kell.self nagy típuson:
másolás/mozgatás történik; nem tulajdonosi szemlélet helyett gyakran
forwarding reference a helyes választás.virtual; runtime polimorfizmushoz
továbbra is virtuális függvény vagy type erasure kell.auto&&-t visszaadó getter temporális objektumon a
temporális tagjára mutató referenciát ad; a hívó élettartama nem
hosszabbodik meg.Az explicit object parameter a tagfüggvény objektumparaméterét a
paraméterlistára hozza, és ezzel dedukálhatóvá teszi a Self
típust. Egy
template<class Self> auto&& f(this Self&& self)
alakkal a const/non-const és lvalue/rvalue overload-négyes egyetlen
sablonná válik; a tagok továbbításához
std::forward_like<Self> használható, amely a
Self-ből viszi át a const- és értékkategória-információt. A
CRTP gyakori static_cast-ja eltűnik, de a class-scope-s
követelmények, a taginjekció és a base referencián keresztüli statikus
polimorfizmus nem váltható ki vele. Rekurzív lambdát tisztán,
std::function nélkül írhatunk. A korlátok élesek: nincs
virtual, static, konstruktor/destruktor, nincs
implicit this, a függvényre nem írható cv/ref minősítő, a
tagfüggvény-pointer és az ABI pedig külön ellenőrizendő. A C++20-as
fallback továbbra is a négy overload, a CRTP és a
std::function/Y-combinator.
Adott a következő C++20-as típus:
struct Box {
int value;
int& get() & { return value; }
const int& get() const& { return value; }
int&& get() && { return std::move(value); }
const int&& get() const&& { return std::move(value); }
};Írd át egyetlen explicit object parameteres függvénysablonra, amely
mind a négy esetben a megfelelő referenciatípust adja vissza. Ellenőrizd
static_assert-tel legalább a nem const lvalue, a const
lvalue és a nem const rvalue esetet. Írd le, miért nem elegendő a
std::forward<Self>(self).value a tag eléréséhez, ha a
tag egy std::vector eleme.
Az alábbi kódot egy kolléga azzal a céllal írta, hogy a
Buffer::at a négyes overloadot váltsa ki:
struct Buffer {
std::vector<int> data;
template<class Self>
auto&& at(this Self&& self, std::size_t i) {
return std::forward<Self>(self).data[i];
}
};Mondd meg, fordul-e, és ha igen, miért nem adja vissza az rvalue
esetben int&&-t. Javítsd a kódot. Milyen futásidejű
veszélyt jelent, ha a javított gettert temporális Buffer-en
hívjuk meg és a visszaadott referenciát eltároljuk?
Adott a CRTP-s alak:
template<class D>
struct Base {
void interface() const { static_cast<const D*>(this)->impl(); }
};
struct Derived : Base<Derived> {
void impl() const {}
};Írd át explicit object parameterrel. Ezután válaszolj: mi történik,
ha Base const& b = d; b.interface(); formában hívjuk
meg? Miért viselkedik másképp a CRTP-s változat? Sorolj fel legalább két
olyan feladatot, amelyre a CRTP továbbra is jobb választás.
#include <type_traits>
#include <utility>
struct Box {
int value;
template<class Self>
auto&& get(this Self&& self) {
return std::forward_like<Self>(self.value);
}
};
int main() {
Box b{1};
const Box cb{2};
static_assert(std::is_same_v<decltype(b.get()), int&>);
static_assert(std::is_same_v<decltype(cb.get()), const int&>);
static_assert(std::is_same_v<decltype(Box{3}.get()), int&&>);
static_assert(std::is_same_v<decltype(std::move(cb).get()), const int&&>);
}A Self a hívó objektum const- és értékkategóriáját
hordozza; a forward_like ezt viszi át a value
tagra. A std::forward<Self>(self).value közvetlen
tagelérésnél működne, mert a tagelérés megőrzi az értékkategóriát, de
egy std::vector operator[]-je nem
ref-minősített: rvalue vektoron is int&-t ad, ezért ott
a forward_like szükséges. Ellenőrzési feltétel: a négy
static_assert forduljon le, és a const rvalue esetben az
eredmény const int&& legyen.
A kód lefordul, de a visszatérési típus rvalue Buffer
esetén is int& lesz. Az ok a
std::forward<Self>(self).data[i]kifejezésben van: a data[i] hívás a nem ref-minősített
std::vector::operator[]-t használja, amely lvalue
referenciát ad. A value category nem „szivárog át” a subscripten.
Javítás:
template<class Self>
auto&& at(this Self&& self, std::size_t i) {
return std::forward_like<Self>(self.data[i]);
}A forward_like a Self alapján castolja az
int& kifejezést int&&-re, illetve
const esetben const int&&-re. Ha a gettert
temporális Buffer-en hívjuk, és a visszaadott referenciát
eltároljuk, a temporális a teljes kifejezés végén megszűnik; a
referencia dangling lesz, és a későbbi használat nem definiált
viselkedés. Ellenőrzési feltétel:
static_assert(std::is_same_v<decltype(Buffer{{1}}.at(0)), int&&>)
teljesüljön, és a temporálisra visszaadott referenciát ne tároljuk
el.
struct Base {
template<class Self>
void interface(this Self const& self) {
self.impl();
}
};
struct Derived : Base {
void impl() const {}
};A Derived d; d.interface(); hívásban
Self = Derived, és a self.impl() megtalálható.
A Base const& b = d; b.interface(); hívásban viszont a
statikus típus Base, ezért Self = Base, és a
Base-nek nincs impl() tagja; a
requires/static_assert itt hibát jelez. A
CRTP-s Base<Derived> azért működik, mert a base már a
példányosításkor ismeri a Derived típust, és a
static_cast a dinamikus objektumra mutat. A deducing this a
statikus típusból dedukál, nem a dinamikusból.
A CRTP továbbra is jobb, ha (1) a base-nek class-scope-ban kell a
derived típusra hivatkoznia, typedefet/tagot kell örökítenie vagy
példányosításkor kell követelményt ellenőriznie; (2) a base referencia
mögül is a derived statikus interfészét akarjuk hívni; (3) olyan
statikus polimorf kódot írunk, ahol a derived típus a base
típusparamétere, és erre a template-metaprogramozásban is szükség van.
Ellenőrzési feltétel: a Derived d; d.interface(); hívás
forduljon, a Base const&-es hívás pedig adjon érthető
diagnosztikát, ne csendben a Base-re dedukáljon.