A C++20-ban egy állapot nélküli függvényobjektumot (funktort) csak
körülményesen lehetett igazán „súlytalanná” tenni: a tagfüggvénynek
mindig van egy rejtett this paramétere, még akkor is, ha az
objektumnak nincs állapota. Ugyanígy a többdimenziós indexelést
operator()(i, j) vagy at(i, j) formában
kellett megoldani, mert az operator[] csak egy paramétert
fogadott el. A C++23 nyelvi változásai közül ebben a részben a hívó- és
indexelő operátorok bővítését, valamint a lambdák ehhez kapcsolódó
finomításait tárgyaljuk. A cél nem a C++20-as operátor- és lambdaalapok
újratanítása — azokat lásd az Operátortúlterhelés és
konverziós operátor (23.3) és a Lambda, capture
és closure lifetime (30.1) részekben —, hanem annak megértése, mit
változtat a C++23, milyen szerződéseket kell ilyenkor kézben tartani, és
hogyan lehet a képességet feature-test makróval és compile probe-bal
ellenőrizni.
A statikus hívóoperátor
(static operator()) C++23-as nyelvi képesség: egy osztály
tagfüggvényeként definiált operator() lehet
static. A pontos feltételek a következők. A függvénynek
tagfüggvénynek kell lennie, és a static kulcsszót a
deklarációban kell szerepeltetni. Statikus tagfüggvényként nem lehet
const, volatile, & vagy
&& minősítésű, mert ezek a minősítők a rejtett
this paraméterre vonatkoznak, amely itt nem létezik. Nem
lehet virtual sem, mert a virtuális dispatchhez
objektumpéldány kell. Lehet viszont constexpr,
consteval és noexcept, és a hívás szintaxisa a
hívó oldalon változatlan: f(args).
A képesség a P1169 paperszámhoz kötődik a témakör tervében. A
támogatás tényleges meglétét nem szabad feltételezni: a laborprofilban
rögzített GCC vagy Clang verzióval feature-test makróval és egy kis
compile probe-bal kell ellenőrizni. A makró neve
__cpp_static_call_operator, a C++23-as érték
202207L. A vizsgálat tipikus alakja:
#if defined(__cpp_static_call_operator) && __cpp_static_call_operator >= 202207L
# define CPP23_STATIC_CALL_OP 1
#else
# define CPP23_STATIC_CALL_OP 0
#endifA legfontosabb gyakorlati következmény a capture nélküli
lambda esetére vonatkozik. A C++23 szerint az ilyen lambda
closure típusának operator()-je implicit módon
static: a closure üres típus, a hívás nem igényel
this paramétert. Ezt a felhasználó nem írja ki, és a
mutable-lel nem kombinálhatja, mert a mutable
éppen a nem-const this-t jelentené. A static
és a mutable tehát kölcsönösen kizárja egymást.
A this paraméter elhagyásának van hívási konvencióbeli
hatása. Az x86-64 System V ABI-ban egy nem-static tagfüggvény első
integer paramétere a this (jellemzően az RDI
regiszterben), a többi paraméter pedig eggyel eltolódik. Egy statikus
operator() esetén ez a plusz argumentum eltűnik, ami
regisztert takaríthat meg, és egyszerűbbé teheti a függvénypointeres
átadást. Fontos azonban, hogy ez elvi, nem mért hatás: a fordító az
inline-olt, optimalizált kódban gyakran teljesen eltünteti a
különbséget. Mérés nélkül nem állítható, hogy a statikus változat
gyorsabb; a laborban külön kell megmérni, és a mérési eredményt a
fordítóverzióhoz és az optimalizálási szinthez kell kötni.
ABI-szempontból a statikus és a nem-static operator()
mangled neve eltér, és a hívási konvenció is más. Ha egy headerben egy
meglévő operator()-t static-ká teszünk, a
bináris interfész megváltozik: a régi és az új könyvtár nem lesz
link-kompatibilis. Ez DevOps-környezetben különösen fontos, ha a
függvény egy megosztott könyvtár határán van. Új kódban a statikus
változat bevezetése nem jelent ABI-törést, de a meglévő API-t nem szabad
csendben átminősíteni.
A lambdákra vonatkozó C++23-as változások közül kettő kapcsolódik
ide. Az első a capture nélküli lambda implicit statikus
operator()-je, amelyet az előző pontban tárgyaltunk. A
második a lambda-deklarátor egyszerűsítése: a
paraméterlista elhagyható, ha a lambda nem vesz át paramétert, és nincs
olyan specifikátor, amely a paraméterlistát igényelné. A
[]{} és a [](){} alak tehát egyenértékű, de a
[] mutable {} nem érvényes, mert a mutable a
paraméterlistához kötött. Hasonlóképpen a noexcept, a
constexpr, a consteval és a
static specifikátorok bizonyos kombinációi igénylik a
paraméterlista jelenlétét.
A lambda-attribútumok terén a C++23 lehetővé teszi, hogy attribútumok
a lambda-introducer után is megjelenjenek, például
[] [[nodiscard]] () { return 1; } alakban. A pontos
elfogadott szintaxist és a paraméterlista elhagyhatóságának eseteit
azonban nem szabad fejből feltételezni: ezek verzió- és compilerfüggő
részletek, amelyeket compile probe-bal kell ellenőrizni. A
__cpp_lambdas makró C++23-as értéke a rendelkezésre álló
jegyzet szerint 202207L, de ez is ellenőrizendő.
A többdimenziós indexelő operátor (P2128) lehetővé
teszi, hogy az operator[] tagfüggvénynek egynél több
paramétere legyen. A szintaxis változatlanul a[i, j], de a
vessző jelentése megváltozik: ha az osztálynak van többparaméteres
operator[]-ja, akkor a vessző paraméterelválasztó, nem
vesszőoperátor. Ha nincs ilyen, akkor a C++20-ig megszokott módon a
vesszőoperátor lép életbe.
Ez a különbség könnyen félreértéshez vezet. A beépített
tömbindexelésnél az operator[] továbbra is egyoperandusú,
ezért
int a[10];
a[1, 2] = 5; // a[2] = 5: a vesszőoperátor kiértékeli 1-et, eldobja, majd 2-t adC++23-ban is ezt jelenti, mert a beépített [] nem
többparaméteres. Saját osztálynál viszont a m[1, 2] attól
függ, van-e többparaméteres operator[]. Ha van, két
paraméteres hívás; ha nincs, a vesszőoperátor eredménye kerül az
egyparaméteres []-ba. Ha az osztálynak nincs egyparaméteres
operator[]-ja, a m[1, 2] fordítási hiba lesz.
A vesszőoperátor túlterhelése ezen nem változtat: a beépített
[] esetén továbbra is a operator, hívódik, ha
azt az osztály túlterhelte.
A következő példa egy egyszerű, sorfolytonos mátrixtípust mutat,
amely C++23-ban m[i, j] alakban indexelhető. A kód önállóan
fordítható, de a C++23-as operator[] miatt a laborprofilban
rögzített, a képességet támogató compilerverzió szükséges hozzá.
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <vector>
class Matrix {
public:
Matrix(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols) {
if (cols_ != 0 && rows_ > std::numeric_limits<std::size_t>::max() / cols_) {
throw std::length_error("Matrix: meret tulcsordulas");
}
data_.resize(rows_ * cols_);
}
int& operator[](std::size_t i, std::size_t j) noexcept {
return data_[i * cols_ + j];
}
int operator[](std::size_t i, std::size_t j) const noexcept {
return data_[i * cols_ + j];
}
int& at(std::size_t i, std::size_t j) {
if (i >= rows_ || j >= cols_) {
throw std::out_of_range("Matrix: index hataron kivul");
}
return (*this)[i, j];
}
int at(std::size_t i, std::size_t j) const {
if (i >= rows_ || j >= cols_) {
throw std::out_of_range("Matrix: index hataron kivul");
}
return (*this)[i, j];
}
std::size_t rows() const noexcept { return rows_; }
std::size_t cols() const noexcept { return cols_; }
private:
std::size_t rows_;
std::size_t cols_;
std::vector<int> data_;
};A (*this)[i, j] itt már a többparaméteres
operator[] hívása. A const változat értéket ad
vissza, nem referenciát; ezzel elkerüljük, hogy egy temporary
Matrix-on kapott referenciát a hívó tovább éltessen. A
nem-const változat int&-t ad, ami a data_
elemére hivatkozik, és a Matrix élettartamához kötött.
A build parancs a laborprofilban rögzített compilerrel:
g++ -std=c++23 -O2 -Wall -Wextra -Wpedantic matrix.cpp -o matrix
clang++ -std=c++23 -O2 -Wall -Wextra -Wpedantic matrix.cpp -o matrixEz a parancs a szándékot rögzíti, nem annak bizonyítéka, hogy a laborban telepített compiler támogatja a képességet; a tényleges támogatást a feature-test makró és a compile probe dönti el.
Az operator[] előfeltétele, hogy
i < rows_ és j < cols_ teljesüljön. Ezt
a függvény nem ellenőrzi, mert a gyorsított indexelés éppen ettől gyors;
a szerződés megsértése nem definiált viselkedés, nem kivétel. A
noexcept ezért indokolt: a
std::vector::operator[] nem dob, és a saját
operator[] sem végez ellenőrzést. A at() ezzel
szemben ellenőriz, és std::out_of_range-t dob, ezért nem
noexcept.
Az indexszámításnál az i * cols_ + j alak
túlcsordulhatna, ha i és cols_ nagyon nagy. A
konstruktor azonban ellenőrzi, hogy rows_ * cols_ ne
csorduljon túl. Ebből következik, hogy a szerződés betartása esetén az
indexszámítás sem csordul túl:
és mivel
,
az
legfeljebb
.
Ez a vázlat arra, hogy a bounds-szerződés és az
overflow-ellenőrzés ugyanannak a méretszerződésnek a két oldala: a
konstruktor a teljes méretet, az operator[] a két indexet
feltételezi.
Az std::mdspan C++23-as operator[]-ja
szintén több indexet fogad, és ugyanezt a mintát követi: a méreteket és
a layoutot az mdspan példány hordozza, az indexelés pedig
nem végez bounds check-et. Az mdspan részleteivel a Multidimenziós view és numeric utility térkép
(31.5) foglalkozik; itt csak a kapcsolatot rögzítjük.
A többdimenziós operator[] feature-test makrója
__cpp_multidimensional_subscript, C++23-as értéke a jegyzet
szerint 202110L. A statikus operator() makrója
__cpp_static_call_operator (202207L). A
statikus operator[] (P2589) önálló makróját a rendelkezésre
álló jegyzet nem nevezi meg, ezért itt compile probe dönt: egy
static int operator[](int) deklarációt tartalmazó kis
fordítási egységgel kell ellenőrizni, hogy a compiler elfogadja-e.
A C++20-as fallback a következő. Statikus operator()
helyett használhatunk statikus tagfüggvényt más néven, például
static bool apply(int), vagy névtelen névtérben szabad
függvényt. Többdimenziós operator[] helyett
operator()(i, j) vagy at(i, j) áll
rendelkezésre. A kódot feltételes fordítással lehet mindkét világban
fordítani:
#if defined(__cpp_multidimensional_subscript) && __cpp_multidimensional_subscript >= 202110L
# define CPP23_MULTIDIM_SUBSCRIPT 1
#else
# define CPP23_MULTIDIM_SUBSCRIPT 0
#endif
class Grid {
public:
int& at(std::size_t i, std::size_t j) { return data_[i * cols_ + j]; }
#if CPP23_MULTIDIM_SUBSCRIPT
int& operator[](std::size_t i, std::size_t j) noexcept { return data_[i * cols_ + j]; }
#else
int& operator()(std::size_t i, std::size_t j) noexcept { return data_[i * cols_ + j]; }
#endif
private:
std::size_t cols_ = 0;
std::vector<int> data_;
};Ez a minta a lényeg: a hívó oldalnak a fallback ágban
g(i, j), a C++23-as ágban g[i, j] alakot kell
használnia, ezért a választást vagy makróval, vagy külön wrapperrel kell
egységesíteni. A #if ágakban a két operator
szignatúra nem azonos, ezért a kód más részein is figyelni kell a hívási
szintaxisra.
Az első és leggyakoribb hiba a static és a
const együttes használata:
static int operator()(int) const nem érvényes, mert a
statikus tagfüggvénynek nincs this paramétere, amit a
const minősíthetne. A második a m[1, 2]
félreértelmezése: C++20-as kódban ez a vesszőoperátor miatt
m[2]-t jelenthet, C++23-ban viszont két paraméteres hívást,
ha van ilyen operator[]; a kettő közötti váltás csendben
megváltoztathatja a program jelentését. A harmadik a bounds check
elhagyása és az overflow figyelmen kívül hagyása: az
operator[] nem ellenőriz, a méretösszeszorzást a
konstruktornak kell védenie. A negyedik a const
operator[] által visszaadott referencia élettartamának
félreértése: egy temporary Matrix-on kapott
const int& danglinggé válik, amint a temporary
megszűnik. Az ötödik a feature-test makró elhagyása: a kód látszólag
fordul, de egy régebbi compileren nem a várt ágat választja.
Kódolási feladat. Definiálj egy
IsEven funktort
static constexpr bool operator()(int) noexcept
hívóoperátorral, és használd std::count_if-fel egy
std::vector<int>-en. Ellenőrizd
static_assert-tel, hogy a hívás konstans kifejezésben is
működik. Indokold, miért lesz a funktor típusa üres, és miért nem
igényel a hívás this paramétert.
Tervezési feladat. Egészítsd ki a
Matrix osztályt rows() és cols()
tagfüggvényekkel, valamint egy at(i, j) const változattal.
Biztosítsd, hogy a m[1, 2] és a m.at(1, 2)
ugyanazt az elemet adja, és hogy a m.at(3, 0)
std::out_of_range-t dobjon. Fogalmazd meg az
operator[] előfeltételét és a konstruktor
méretellenőrzésének szerepét.
Diagnosztikai feladat. Adott egy C++20-as
osztály, amely m(i, j) alakban indexel. Írd át C++23-as
m[i, j] alakra, és készíts olyan feltételes fordítású
változatot, amely C++20-on a régi m(i, j) szintaxist
használja. Mutasd meg, melyik feature-test makró dönt, és hogyan
ellenőriznéd a statikus operator[] támogatását.
#include <algorithm>
#include <cassert>
#include <vector>
struct IsEven {
static constexpr bool operator()(int x) noexcept { return x % 2 == 0; }
};
static_assert(IsEven{}(4));
static_assert(!IsEven{}(3));
int main() {
std::vector<int> v{1, 2, 3, 4, 6};
assert(std::count_if(v.begin(), v.end(), IsEven{}) == 3);
}A static miatt a hívás nem igényel this
paramétert, a funktor típusa üres, és a static_assert azt
igazolja, hogy a hívás konstans kifejezésben is érvényes.
Ellenőrzési feltétel: a program
-std=c++23-mal forduljon, a static_assert-ek
menjenek át, és a count_if eredménye 3 legyen. C++20-on a
static elhagyásával a kód továbbra is fordul, de a hívás
ekkor this-t igényel.
#include <cassert>
#include <stdexcept>
int main() {
Matrix m(3, 4);
m[1, 2] = 42;
assert((m[1, 2] == 42)); // dupla zárójel: a vessző ne makróargumentum-elválasztó legyen
assert(m.at(1, 2) == 42);
bool dobott = false;
try {
(void)m.at(3, 0);
} catch (const std::out_of_range&) {
dobott = true;
}
assert(dobott);
}Az assert makró, és a preprocesszor a C++ nyelvtannál
korábban dolgozik: a makróhívás argumentumait a legfelső szintű,
zárójelbe nem foglalt vesszők választják el, és a szögletes zárójel
ebből a szempontból nem számít. Az assert(m[1, 2] == 42)
ezért két makróargumentumot kapna (m[1 és
2] == 42), és a fordítás
macro "assert" passed 2 arguments, but takes just 1 hibával
leáll. A külön zárójelpár egyetlen argumentummá fogja össze a
kifejezést; ugyanez kell minden más függvényszerű makrónál is, ha az
argumentumban a C++23-as többargumentumos operator[]
szerepel.
Buildjegyzőkönyv (lab-gcc profil). A
fenti Matrix-szal kiegészítve a program
g++ -std=c++23 -Wall -Wextra -Wpedantic beállítással
figyelmeztetés nélkül fordult és futott, ASan/UBSan builddel is. A
zárójel nélküli assert a fenti makróhibát adta. C++20-ban a
kétparaméteres operator[] deklarációja maga hibás
(must have exactly one argument), ezért a fallback
szükséges.
Az operator[] előfeltétele
i < rows_ && j < cols_; megsértése nem
definiált viselkedés, ezért nem dob. Az at() ellenőriz, és
std::out_of_range-t dob. A konstruktor a
rows_ * cols_ szorzatot ellenőrzi, mert e nélkül a későbbi
i * cols_ + j túlcsordulhatna. Ellenőrzési
feltétel: a m[1, 2] és a m.at(1, 2)
értéke egyezzen, a határon kívüli at() dobjon, és a program
szaniterrel (például -fsanitize=address,undefined) is
futási hibát ne jelezzen.
A feltételes fordfítás kulcsa a
__cpp_multidimensional_subscript makró. Ha a makró értéke
legalább 202110L, a C++23-as operator[] ág
fordul; különben a C++20-as operator() ág. A hívó oldalt
ugyanígy kell elágaztatni, vagy egy vékony wrapperrel egységesíteni:
#if CPP23_MULTIDIM_SUBSCRIPT
m[i, j] = 7;
#else
m(i, j) = 7;
#endifA statikus operator[] támogatását külön compile
probe-bal kell ellenőrizni, mert a jegyzet nem nevez meg rá önálló
makrót. Ellenőrzési feltétel: a C++23-as ág a támogató
compileren forduljon és futáskor a helyes elemet írja; a C++20-as ág a
régi compileren forduljon; a makró nélküli, véletlenül rossz ágat
választó buildet a CI-ben egy negatív teszt fogja meg.
A C++23 két, látszólag kicsi operátorbővítése ugyanazt a célt
szolgálja: a hívási és indexelési szintaxis közelebb kerüljön ahhoz,
amit a programozó gondol. A static operator() eltünteti a
felesleges this paramétert az állapot nélküli
callable-ökből, és a capture nélküli lambdák operator()-jét
is statikussá teszi. A többdimenziós operator[] természetes
m[i, j] szintaxist ad, de megváltoztatja a vessző
jelentését, ezért a régi kóddal való keveredés külön figyelmet igényel.
Mindkét képességnél a bounds-szerződés, a méret-overflow és az ABI-határ
a lényeg; a támogatást feature-test makróval és compile probe-bal kell
ellenőrizni, a C++20-as fallbacket pedig tudatosan kell megtervezni.