A C++23 nyelvi változásainak egy része nem új szintaxist vezet be,
hanem az élettartam- és értékkategória-szabályokat finomítja. Ez a rész
négy ilyen változást tárgyal: az auto(x) explicit
decay-copy-t, az egyszerűsített implicit move-ot, a range-for
temporary-élettartam-kiterjesztését, valamint a [[assume]]
és a std::unreachable optimalizálási szerződéseit. A
vezérmotívum a dangling: mindegyik változás arról szól, hogy egy érték
vagy egy rá mutató nézet meddig érvényes, és a fordító mikor
feltételezheti, hogy a programozó ezt betartotta. A rész a Temporary, value category és dangling (20.4)
részben bevezetett temporary- és value category-fogalmakra, a Borrowed range, dangling és iterator lifetime
(30.5) részben tárgyalt borrowed range-ekre és az Optimalizáló, as-if és UB következményei (14.3)
részben bevezetett UB- és as-if-modellre épül; ezeket itt nem ismételjük
teljesen, csak a szükséges előfeltételeket idézzük fel.
A C++23-as képességek fordító- és könyvtártámogatása nem azonos. A fejezetterv kiinduló URL-jei (a WG21 munkatervezet, a cppreference C++23-oldala, a GCC/Clang státuszoldalak) kutatási kiindulópontok, nem bizonyítékok; a konkrét támogatást a laborban, compile probe-pal kell ellenőrizni. A feature-test makrók neve és a hozzá tartozó képesség:
| Képesség | Feature-test makró | Jelleg |
|---|---|---|
auto(x) |
__cpp_auto_cast |
nyelvi |
| implicit move | __cpp_implicit_move |
nyelvi |
| range-for élettartam | __cpp_range_based_for |
nyelvi; C++20-ban is definiált, más értékkel |
[[assume]] |
__cpp_assume |
nyelvi |
std::unreachable() |
__cpp_lib_unreachable |
könyvtári |
A következő probe önmagában fordítható C++20-ban és C++23-ban is; a
hiányzó makrókat nem definiáltként jelzi. A szakirodalomban elterjedt
értékek (például __cpp_auto_cast = 202110L,
__cpp_implicit_move = 202207L,
__cpp_range_based_for = 202211L,
__cpp_assume = 202207L,
__cpp_lib_unreachable = 202202L) irodalmi értékek,
amelyeket a probe kimenetével és a hivatkozott WG21-paperrel kell
összevetni; a fejezet írásakor ezek nem tekintendők ellenőrzöttnek.
// probe.cpp
#include <version>
#include <cstdio>
int main() {
#ifdef __cpp_auto_cast
std::printf("__cpp_auto_cast = %ld\n", __cpp_auto_cast);
#else
std::printf("__cpp_auto_cast nem definiált\n");
#endif
#ifdef __cpp_implicit_move
std::printf("__cpp_implicit_move = %ld\n", __cpp_implicit_move);
#else
std::printf("__cpp_implicit_move nem definiált\n");
#endif
#ifdef __cpp_range_based_for
std::printf("__cpp_range_based_for = %ld\n", __cpp_range_based_for);
#else
std::printf("__cpp_range_based_for nem definiált\n");
#endif
#ifdef __cpp_assume
std::printf("__cpp_assume = %ld\n", __cpp_assume);
#else
std::printf("__cpp_assume nem definiált\n");
#endif
#ifdef __cpp_lib_unreachable
std::printf("__cpp_lib_unreachable = %ld\n", __cpp_lib_unreachable);
#else
std::printf("__cpp_lib_unreachable nem definiált\n");
#endif
}g++ -std=c++20 -O2 -Wall -Wextra -o probe20 probe.cpp && ./probe20
g++ -std=c++23 -O2 -Wall -Wextra -o probe23 probe.cpp && ./probe23
# Clang esetén: clang++ -std=c++20 / -std=c++23A kimenet értelmezése: ami C++20 módban „nem definiált”, az C++23 módban vagy definiált, vagy a fordító még nem támogatja. Utóbbi esetben a képességhez fallback kell, nem a makróra épülő feltétel nélküli kód.
auto(x) és
az auto{x} decay-copyA C++20-ig a „decay-copy” a standard szövegében létező,
exposition-only fogalom volt: a std::ranges egyes részei és
a generikus kód nem tudta kifejezni, hogy „az argumentumból független,
másolt értéket kérek”. A P0849 paperszámú javaslat ezt nyelvi eszközzé
tette.
Definíció. Az auto(x) egy prvalue
kifejezés, amelynek típusát az auto típuslevezetése adja
meg az x operandusból. A típuslevezetés a by-value
paraméterátadás szabályait követi: a referencia eldobása, a top-level
const és volatile eldobása, a tömb- és
függvénytípus pointerré alakítása. Az eredmény egy új objektum, amely
független az operandustól; a másolat akkor készül el, amikor a prvalue
materializálódik. Az auto{x} ugyanezt teszi, de
lista-inicializálással, ezért tiltja a szűkítő konverziót.
Feltételek és következmények. Az
auto(x) mindig prvalue, soha nem referencia és soha nem
xvalue; nem azonos a decltype(auto)-val, amely referenciát
is adhat. Nem őrzi meg a top-level const-ot:
const int i = 5; auto j = auto(i); esetén j
típusa int. A másolat költsége akkor jelenik meg, amikor az
érték anyagiassá válik; visszatérési értékként a guarantee copy elision
miatt gyakran nincs tényleges másolás, de ez nem általános garancia.
// auto_decay_copy.cpp
#include <cstdio>
#include <string>
#include <type_traits>
#include <utility>
template<class T>
auto decay_copy(T&& t) {
#if defined(__cpp_auto_cast) && __cpp_auto_cast >= 202110L
return auto(std::forward<T>(t)); // C++23
#else
return std::decay_t<T>(std::forward<T>(t)); // C++20 fallback
#endif
}
int main() {
const std::string s = "hello";
auto a = decay_copy(s);
static_assert(std::is_same_v<decltype(a), std::string>);
std::printf("%s\n", a.c_str());
}g++ -std=c++23 -O2 -Wall -Wextra -o decay23 auto_decay_copy.cpp && ./decay23
g++ -std=c++20 -O2 -Wall -Wextra -o decay20 auto_decay_copy.cpp && ./decay20A tipikus hiba itt az, hogy a programozó referenciát vagy
const-ot vár vissza. Az auto(x) tudatosan
értéket ad: ez a független, biztonságos másolat eszköze, nem a
nézeté.
A C++20 szabálya (P1825) szerint a return e;
utasításban, ha e egy nem-volatile, automatikus tárolási
idejű objektum vagy rvalue-referencia paraméter neve, és a visszatérési
típus nem referencia, akkor a fordító először úgy végzi a
túlterhelés-feloldást, mintha e rvalue lenne, és csak ennek
kudarca esetén próbálja lvalue-ként. Ez a szabály már C++20-ban sem
követeli meg, hogy a visszatérési típus megegyezzen e
típusával: egy Derived helyi változó Base-ként
visszaadva C++20-ban is a Base(Base&&) konstruktort
választja. (A GCC ezt a javítást korábbi nyelvi módokra is
alkalmazza.)
Definíció. A C++23 (P2266) egyszerűsíti a szabályt:
a return e; utasításban a fenti feltételeknek megfelelő
e név egyszerűen xvalue, a két lépcsős
túlterhelés-feloldás helyett. Az új szabály a referencia visszatérési
típusú függvényekre is vonatkozik, és éppen ott változtat
megfigyelhetően. Tömören: C++23-ban a return e; utasításban
az e xvalue, ha
e egy nem-volatile, automatikus tárolási idejű
objektum vagy rvalue-referencia neve, ésreturn egy függvényben (vagy lambdában) áll.Viselkedésváltozás. Érték szerinti visszatérésnél a C++20-hoz képest nincs eltérés; referencia visszatérési típusnál viszont van:
// implicit_move.cpp — C++20 és C++23: mi változott valójában?
#include <cstdio>
#include <utility>
struct Base {
Base() = default;
Base(const Base&) { std::puts("copy"); }
Base(Base&&) noexcept { std::puts("move"); }
};
struct Derived : Base { };
Base f() {
Derived d;
return d; // C++20 és C++23: move (P1825, C++20)
}
Base g() {
const Derived d;
return d; // mindkét módban copy: const Derived&& nem köt Base&&-hez
}
#if __cplusplus > 202002L
int&& fwd(int&& x) { return x; } // C++23: x xvalue, fordul; C++20: hiba
#endif
// int& keep(int&& x) { return x; } // C++20: fordul; C++23: hiba
int main() {
(void)f();
(void)g();
#if __cplusplus > 202002L
int v = 7;
int&& r = fwd(std::move(v));
std::printf("fwd=%d\n", r);
#endif
}g++ -std=c++20 -O0 -Wall -Wextra -o im20 implicit_move.cpp && ./im20
g++ -std=c++23 -O0 -Wall -Wextra -o im23 implicit_move.cpp && ./im23Buildjegyzőkönyv (lab-gcc profil). A
-std=c++20 kimenet: move, copy; a
-std=c++23 kimenet: move, copy,
fwd=7. A f() mindkét módban mozgat, a
const változat (g()) mindkét módban másol. Az
int&& fwd(int&& x) { return x; } C++20-ban
cannot bind rvalue reference ... to lvalue hibát ad,
C++23-ban fordul. A kikommentezett
int& keep(int&& x) { return x; } fordítva
viselkedik: C++20-ban fordul, C++23-ban
cannot bind non-const lvalue reference ... to an rvalue
hibát ad. Ez az utóbbi a migráció valódi forráskompatibilitási
törése.
A return std::move(x); írásmód érték szerinti
visszatérésnél felesleges, sőt hátrányos lehet, mert a
std::move függvényhívása megakadályozza az NRVO-t; a
-Wpessimizing-move figyelmeztet rá.
A C++20-as range-for lényegében
auto&& __range = range-init; deklarációvá alakult.
A range-init azon temporary-jei, amelyek közvetlenül a
__range referenciához kötődtek, a ciklus végéig éltek; a
közvetett temporary-k azonban a teljes kifejezés végén megsemmisültek.
Ez a különbség okozta a klasszikus danglinget.
Definíció. A C++23-ban (P2718) a range-initializerben létrejövő temporary objektumok élettartama a ciklus teljes élettartamára kiterjed, nem csak a range-initializer kiértékelésének végéig. Ez a temporary lifetime extension kiterjesztése a range-for-ra. A szabály csak a range-initializerre vonatkozik; a ciklustörzsben keletkező temporary-kre és az init-statement temporary-jeire nem.
// range_for_lifetime.cpp
#include <vector>
#include <cstdio>
struct Config {
std::vector<int> values{1, 2, 3};
const std::vector<int>& get() const { return values; }
};
Config make() { return Config{}; }
int main() {
int sum = 0;
for (int v : make().get()) { // C++20: dangling; C++23: OK
sum += v;
}
std::printf("sum = %d\n", sum);
}g++ -std=c++20 -g -O1 -fsanitize=address -o r20 range_for_lifetime.cpp && ./r20
g++ -std=c++23 -g -O1 -fsanitize=address -o r23 range_for_lifetime.cpp && ./r23C++20-ban a make() temporary nem közvetlenül a
__range-hez kötődik, hanem a get() hívás
argumentumaként jön létre, ezért a teljes kifejezés végén megsemmisül; a
get() által visszaadott referencia dangling, az iterálás
UB. C++23-ban a make() temporary a ciklus végéig él, a
referencia érvényes marad. A várt laboreredmény: az r20
ASan heap-use-after-free hibát jelez, az r23 normálisan
lefut; a tényleges kimenetet a laborban kell rögzíteni, ez a leírás nem
mérési eredmény.
A P2718 nem old meg minden danglinget. Ha a get() saját
temporary-t ad vissza referenciaként
(const std::vector<int>& get() const { return std::vector<int>{1,2,3}; }),
az már a get() visszatérésekor dangling. Ha a range egy
std::string_view, amely egy lokális stringre mutat, az is
dangling marad. A szabály csak a range-initializer temporary-jeire
terjed ki.
[[assume]]
és a std::unreachableMindkét eszköz a fordítónak tett ígéret, amelynek megsértése nem definiált viselkedés. A különbség az ígéret tartalmában van.
[[assume(expr)]]. A C++23-ban egy null
utasításra alkalmazott attribútum. Azt jelenti: a programozó állítja,
hogy expr a végrehajtás ezen pontján igaz. Ha nem igaz, a
viselkedés nem definiált. Az expr nem értékelődik ki, tehát
nem lehet mellékhatása, és nem hívódik meg; a fordító az igazságára
támaszkodva optimalizálhat. Csak akkor használható, ha a feltétel
tényleg garantált; nem helyettesíti a futásidejű ellenőrzést.
std::unreachable(). A C++23-as
<utility> fejléc függvénye. Azt jelenti: a hívás
helye nem érhető el. Ha mégis elérhető, a viselkedés nem definiált. Nem
feltétel, hanem pont; a hívás megtörténik, de a fordító
optimalizálhatja. Tipikus használata a minden enumértéket lefedő
switch után, vagy olyan függvény végén, ahol a fordító nem
látja, hogy nincs visszatérés.
| Szempont | [[assume(expr)]] |
std::unreachable() |
|---|---|---|
| Az ígéret tartalma | a feltétel igaz | a pont nem érhető el |
| Forma | attribútum null utasításon | függvényhívás |
| Kiértékelődik | nem | a hívás megtörténik; elérhetetlen esetben UB |
| Tipikus használat | feltétel optimalizálása | elérhetetlen ág jelölése |
| Fallback | if (!(cond)) __builtin_unreachable(); |
__builtin_unreachable() |
// assume_unreachable.cpp
#include <utility>
#include <cstdio>
enum class Color { Red, Green, Blue };
const char* name(Color c) {
switch (c) {
case Color::Red: return "red";
case Color::Green: return "green";
case Color::Blue: return "blue";
}
std::unreachable(); // C++23
}
int divide(int a, int b) {
[[assume(b != 0)]]; // a hívónak garantálnia kell
return a / b;
}
int main() {
std::printf("%s %d\n", name(Color::Green), divide(6, 3));
}g++ -std=c++23 -O2 -Wall -Wextra -o au23 assume_unreachable.cpp && ./au23
# C++20 fallback: std::unreachable() helyett __builtin_unreachable(),
# [[assume]] helyett if (!(b != 0)) __builtin_unreachable();A switch után a std::unreachable() azért
kell, mert a fordító a switch után is feltételezhet
visszatérést; a hívás jelzi, hogy minden eset le van fedve. Ha később új
enumérték kerül be, és a switch nem kezeli, a
default nélküli switch -Wswitch
figyelmeztetést ad, a std::unreachable() pedig UB-hez
vezet, ha mégis elérhető. Ezért a std::unreachable() nem
véd a jövőbeli bővítés ellen; a -Wswitch és a teljes
lefedés a védelem.
A C++23 két apró, de hasznos szabályt is hozott. Az egyik: egy címke
állhat az összetett utasítás végén, közvetlenül a } előtt;
C++20-ban legalább egy utasítás kellett a címke után. Ez generált kódban
és switch-eknél kényelmes:
switch (code) {
case 1:
handle();
break;
case 2: // C++23: legális, C++20: fordítási hiba
}A másik: az if/switch init-statementje
lehet alias-deklaráció is. C++23-ban:
if (using T = int; sizeof(T) == 4) {
// a T az if feltételében és ágában él
}C++20-ban ez nem fordul; a fallback a deklarációt az if
elé teszi, ami megváltoztatja a T hatókörét. A változás
főleg generikus kódban hasznos, ahol a típust az init-statementben
akarjuk bevezetni.
A labor célja, hogy ugyanazt a forrást C++20 és C++23 módban
fordítsuk, és megfigyeljük a különbséget. A
range_for_lifetime.cpp esetében a C++20-as buildet
AddressSanitizerrel futtatva a várt eredmény heap-use-after-free hiba; a
C++23-as buildben nincs hiba. Az implicit_move.cpp esetében
a különbség a fordításban látszik: a fwd csak C++23-ban
fordul, a keep csak C++20-ban; a puts-kimenet
(move, copy) a két módban azonos. Az
assume_unreachable.cpp esetében a [[assume]]
megsértése UB; a sanitizer nem garantált, hogy diagnosztizálja, ezért a
helyes használatot kódolvasással és a hívói szerződés ellenőrzésével
kell biztosítani. A std::unreachable() elérését egyes
sanitizerek (például Clang -fsanitize=unreachable)
jelezhetik, de ez nem hordozható elvárás.
A labor parancsai a fenti szekciókban szerepelnek. A „várt eredmény” itt a dokumentált viselkedésen alapuló illusztráció, nem mért eredmény; a tényleges kimenetet a tanuló laborja rögzíti, és összeveti a feature-test makrók kimenetével.
auto(x)-et referenciának gondolni.
Az auto(x) mindig prvalue, értéket ad; ha referenciát
akarunk, arra más eszköz kell. Javítás: a szándékot a típusdeklarációban
és a névben jelezzük.return std::move(x); használata.
C++23-ban felesleges, és ronthatja az NRVO-t. Javítás:
return x;.[[assume]] futásidejű ellenőrzésként.
Ha a feltétel nem garantált, UB. Javítás: futásidejű ellenőrzés vagy
szerződéses API, és csak utána [[assume]].std::unreachable() default
ágban. Ha később új érték érkezik, UB. Javítás:
default nélküli switch és
-Wswitch, vagy default ágban
std::abort().A C++23 négy élettartam- és értékkategória-változása egy közös
irányba mutat: a fordító pontosabban tudja, meddig él egy érték, és a
programozó explicitebb eszközökkel fejezheti ki a szándékát. Az
auto(x) független értéket ad, az implicit move a
return utasításban a származtatott és bázistípusok közötti
mozgatást is lehetővé teszi, a range-for temporary-kiterjesztés a
leggyakoribb közvetett danglinget szünteti meg, a
[[assume]] és a std::unreachable() pedig a
feltétel- és elérhetetlenségi ígéreteket választja szét. Egyik változás
sem helyettesíti a szerződések tudatos tervezését; a feature-test makrók
és a sanitizer-labor csak az eszközök helyes használatát segítik.
range_for_lifetime.cpp C++20-as változatában a
make().get() hívás, és mit változtat a C++23. Adj egy olyan
módosítást, amelyet a P2718 nem javít.decay_copy
függvénysablont, amely C++23-ban auto(x)-et, C++20-ban
std::decay_t-t használ. Ellenőrizd feature-test makróval és
static_assert-tel, hogy a visszatérési típus
std::string egy const std::string&
bemenetre.name(Color) függvény a std::unreachable()
hívással. Vezess be egy új Color::Yellow értéket, és
magyarázd el, mi történik, ha a switch nem kezeli. Javítsd
a kódot úgy, hogy a hiba fordítási időben kiderüljön.Base f() { Derived d; return d; } példában melyik
konstruktor hívódik C++20-ban és C++23-ban, és mi a szerepe a
const-nak? Mutass olyan függvényt, amelynek a
fordíthatósága a két mód között megváltozik.1. megoldás. A make() temporary nem
közvetlenül a range-for __range referenciájához kötődik,
hanem a get() tagfüggvény hívásának argumentumaként jön
létre. C++20-ban az ilyen közvetett temporary a teljes kifejezés végén
megsemmisül, így a get() által visszaadott
const std::vector<int>& danglingre mutat; az
iterálás UB. C++23-ban a range-initializer temporary-jei a ciklus végéig
élnek, ezért a referencia érvényes. Nem javítja a P2718 azt az esetet,
amikor a get() saját temporary-t ad vissza referenciaként:
const std::vector<int>& get() const { return std::vector<int>{1,2,3}; }
– itt a temporary a get() visszatérésekor megsemmisül, mert
nem a range-initializerben jött létre. Ellenőrzési feltétel: a C++20-as
ASan-build hibát jelez, a C++23-as nem; a get() módosított
változata C++23-ban is hibát jelez.
2. megoldás. A decay_copy a fenti
auto_decay_copy.cpp szerint épül fel. A lényeg a feltételes
fordítás:
#if defined(__cpp_auto_cast) && __cpp_auto_cast >= 202110L
return auto(std::forward<T>(t));
#else
return std::decay_t<T>(std::forward<T>(t));
#endifAz ellenőrzés
static_assert(std::is_same_v<decltype(decay_copy(s)), std::string>);,
ahol s const std::string&. Indoklás: az
auto eldobja a referenciát és a top-level
const-ot, a std::decay_t ugyanezt teszi, ezért
a két ág típusa megegyezik. Ellenőrzési feltétel: a C++23-as buildben a
__cpp_auto_cast ág fordul, a C++20-asban a fallback;
mindkettőben a static_assert teljesül.
3. megoldás. Ha a Color::Yellow
bekerül, de a switch nem kezeli, a default
nélküli switch a -Wswitch figyelmeztetést
adja, és a std::unreachable() hívás elérhetővé válik, ami
UB. A javítás: hagyjuk el a default ágat, soroljuk fel az
összes enumerátort, és a switch után
std::unreachable()-t használjunk; így az új érték
figyelmeztetést ad. Alternatíva: default ágban
std::abort(), amely nem UB, hanem definiált leállás.
Ellenőrzési feltétel: az új enumerátorral a build -Wswitch
figyelmeztetést ad, a std::unreachable() nem érhető el.
4. megoldás. Mindkét módban a
Base(Base&&) hívódik: a C++20 (P1825) már nem
követeli meg a típusegyezést, a C++23 (P2266) pedig xvalue-ként kezeli a
d-t. const Derived d; esetén a
const Derived&& nem köthető
Base&&-hez, ezért mindkét módban másolat készül; ez
a const szerepe. A fordíthatóság referencia visszatérési
típusnál változik: az
int&& fwd(int&& x) { return x; } csak
C++23-ban, az int& keep(int&& x) { return x; }
csak C++20-ban fordul. Ellenőrzési feltétel: a puts-kimenet
a két módban azonos (move, illetve a const
változatban copy), a fwd/keep pár
fordítási eredménye pedig a két módban ellentétes; a
lab-gcc profilban mindezt lefuttattuk.