Az alábbi rész abban segít, hogy a C++20-ról C++23-ra váltás ne látens regresszióként, hanem ellenőrzött migrációként történjen. A C++23 háromféle módon érinti a meglévő kódot: egyes elemeket eltávolít, másokat elavultnak jelöl, és van, ahol a forrás változatlan marad, de a szemantika módosul. Az első kettőt a fordító többnyire kiszűri, a harmadikat nem: azt a build- és tesztmátrixnak kell kimutatnia.
A rész a C++23 laborprofilra épül: a
laborprofil rögzíti a fordítót, a libstdc++/libc++ kiadást és a
-std=c++23 kapcsolót. Az implicit move és a range-for
élettartam részletes nyelvi levezetését itt nem ismétlem meg, csak a
migrációs következményeit tárgyalom; a szemantikát a korábbi rész tartalmazza. Minden itt szereplő
fordítási parancs a tanuló saját laborjában futtatandó; a szöveg nem
állítja, hogy bármely eredményt mértem.
Eltávolított (removed) elem: a C++23 szövege már nem
tartalmazza a deklarációt. A C++23 módban a név nem létezik, ezért a
fordítás hard errorral megáll – ez a legkevésbé kockázatos
eset, mert nem lehet figyelmen kívül hagyni. Elavult
(deprecated) elem: továbbra is a szabvány része, de a jövőbeli
eltávolítás jelöltje. Fontos részlet: az elavulás a szabvány
nyelvén fogalmazódik meg, nem feltétlenül jár
[[deprecated]] attribútummal, így a könyvtári implementáció
dönti el, ad-e diagnosztikát, és ezt makróval ki is kapcsolhatja. Ezért
az elavult elemekre nem lehet vakon -Werror-t építeni.
Viselkedésváltozás (behavior change): a kód fordul
mindkét módban, de mást jelent – más overloadot választ a
túlterhelés-feloldás, máskor pusztul el egy temporary, más diagnosztikát
ad a preprocesszor. Ez az a kategória, ahol a csendes regresszió él, és
ahol a kétmódú összehasonlító teszt a legjobb eszköz.
Végül külön kell kezelni a DR-t (defect report): a visszamenőleg is alkalmazható javítást a fordító a korábbi nyelvi módban is implementálhatja, így a „C++20-as” viselkedés nem garantált. Ezért a migrációs mérés mindig a rögzített laborprofilban, dokumentált fordítóverzióval érvényes.
A C++ korábbi kiadásai tartalmaztak egy szemétgyűjtés-támogató
felületet a <memory> fejlécben:
std::declare_reachable,
std::undeclare_reachable,
std::declare_no_pointers,
std::undeclare_no_pointers, valamint a
std::pointer_safety típus és a
std::get_pointer_safety() függvény. A P2186 alapján ezek
kikerültek a szabványból. A felület gyakorlati problémája az volt, hogy
nem lehetett hordozhatóan implementálni, és a valós programok nem
használták: a declare_reachable lényegében egy „a gyűjtő ne
bántsa ezt a pointert” jelzés volt, ami csak együttműködő, úgynevezett
conservative gyűjtő mellett értelmezhető.
A tipikus törő minta, amelyet tanítási célból külön jelölök – ez a példa szándékosan nem fordítható C++23 módban:
// tanítási példa: C++23-ban nem fordul
#include <memory>
void f() {
int* p = new int(42);
std::declare_reachable(p); // C++23: a név nem létezik
// ... munka a pointerrel ...
std::undeclare_reachable(p);
delete p;
}A javítás nem átírás, hanem törlés: a hívások elhagyhatók, a
tulajdonlás pedig marad explicit new/delete,
vagy – helyesebben – std::unique_ptr. A keresés CI-ban
egyszerű szövegkereséssel is elvégezhető a
declare_reachable|declare_no_pointers|pointer_safety
mintára, de a döntő kapu a C++23-as build maga.
aligned_storage, aligned_union és
has_denormA P1413 a std::aligned_storage és a
std::aligned_union típusokat jelöli elavultnak. Ezek soha
nem voltak teljes értékű eszközök: egy nyers, igazított byte-tömböt
adtak, amelybe placement new-val kellett objektumot tenni, és a
típus-punning körüli szabályok (effective type, laundering) miatt könnyű
volt velük nem definiált viselkedést előállítani. A javasolt irány az
alignas közvetlen használata, illetve a
std::optional<T>, ha a cél egy „esetleg létező
T”.
A std::numeric_limits<T>::has_denorm elavulása
ugyanebbe a családba tartozik: olyan tulajdonságot kérdezett, amely a
gyakorlatban vagy mindig jelen volt, vagy a lekérdezés nem adott
cselekvésre váltható információt, és a válasz erősen implementációfüggő.
Aki mégis a denormális viselkedésre akar építeni, az a
denorm_min() és a min() viszonyát vizsgálja,
illetve a célplatform dokumentált lebegőpontos modelljére támaszkodik;
ezt a laborprofilban kell rögzíteni, mert a szabványos lebegőpontos
szemantika nem írja elő a denormálisok hardveres támogatását.
Az aligned_storage kiváltására egy önállóan fordítható
példa:
// build: g++ -std=c++23 -Wall -Wextra -Wdeprecated -pedantic -o storage storage.cpp
#include <cstddef>
#include <cstdio>
#include <new>
#include <type_traits>
#include <utility>
template <class T>
class InlineStorage {
public:
// A sablon ne legyen jelölt egyetlen saját típusú argumentumra: különben
// egy nem const lvalue-ból a törölt másoló helyett ez nyerne.
template <class... Args>
requires (!(sizeof...(Args) == 1 &&
(std::is_same_v<std::remove_cvref_t<Args>, InlineStorage> && ...)))
&& std::is_constructible_v<T, Args...>
explicit InlineStorage(Args&&... args) {
::new (static_cast<void*>(buf_)) T(std::forward<Args>(args)...);
}
~InlineStorage() { get()->~T(); }
// Az implicit másolás/mozgatás a buf_ BÁJTJAIT másolná, nem a T objektumot:
// a két példány ugyanazt az erőforrást birtokolná (std::string esetén
// kétszeres felszabadítás). A minimális owner ezért nem másolható.
InlineStorage(const InlineStorage&) = delete;
InlineStorage& operator=(const InlineStorage&) = delete;
T* get() { return std::launder(reinterpret_cast<T*>(buf_)); }
private:
alignas(T) std::byte buf_[sizeof(T)];
};
struct Point { int x, y; };
int main() {
InlineStorage<Point> s(1, 2);
std::printf("%d %d\n", s.get()->x, s.get()->y);
}Az owner itt az InlineStorage objektum, a tároló a
buf_ tomb, a get() nem tulajdonló hozzáférés,
az invalidáló művelet pedig a destruktor (utána a pointer dangling). A
std::launder azért kell, mert a
reinterpret_cast által adott pointer eredetileg
std::byte-ra mutatott. Ha nem kell kézi élettartam-kezelés,
a std::optional<Point> rövidebb és
biztonságosabb.
A sablon szerződése nem csak a triviális Point-ra kell,
hogy helyes legyen. Az implicit másoló és mozgató művelet a
buf_ tömböt bájtonként másolná, nem a benne élő
T objektumot konstruálná.
InlineStorage<std::string> esetén a két példány
ugyanazt a heap-puffert birtokolná, és a második destruktor kétszeres
felszabadítást okozna. A minimális owner ezért nem másolható és nem
mozgatható: a másoló műveletek törölve vannak, a mozgatók pedig így
nincsenek deklarálva, és a mozgatás is a törölt másolóra esik. (A másik
lehetőség a T valódi másoló/mozgató konstrukciója a
célpufferben, a megfelelő kivételgaranciával; ez már egy
std::optional-szerű típus újraírása.) A variadikus
konstruktor-sablont korlátozni kell, különben egy nem const
lvalue-ból történő direkt inicializálásnál
(InlineStorage<std::string> b(a);) jobb jelölt lenne
a törölt másolónál.
Buildjegyzőkönyv (lab-gcc profil,
g++ -std=c++23 -Wall -Wextra -Wdeprecated -pedantic). A
program figyelmeztetés nélkül fordult, kimenete 1 2. Az
InlineStorage<std::string> b = a; és a
b(a) alak, std::any elemtípussal is,
use of deleted function hibát ad. Egy
std::string-et tároló példány és egy élő objektumokat
számláló típus ASan/UBSan alatt hibátlanul fut, a számláló a végén 0. A
javítás előtti változatban az
InlineStorage<std::string> másolata ASan alatt
attempting double-free hibát adott.
Az implicit move C++23-as egyszerűsítése (P2266, a Érték, élettartam és kifejezések: auto(x), implicit
move és range-for (40.5) részben tárgyalt módon) migrációs
szempontból elsősorban fordíthatósági változás: a
return utasításban megnevezett implicit mozgatható entitás
xvalue lesz, ezért az
int&& fwd(int&& x) { return x; } C++23-ban
fordul, a lokálisra lvalue-referenciát visszaadó kód pedig nem. Az érték
szerinti paraméter visszaadása viszont nem változik: az
implicit move C++11 óta kiterjed rá. Ezt is érdemes kétmódú teszttel
rögzíteni, mert a „C++23-ban más konstruktor fut” feltételezés gyakori
tévedés:
// passthrough.cpp — build: g++ -std=c++20 ... ; majd ugyanez -std=c++23 ...
#include <cstdio>
struct S {
int copies = 0, moves = 0;
S() = default;
S(const S& o) : copies(o.copies + 1), moves(o.moves) {}
S(S&& o) : copies(o.copies), moves(o.moves + 1) {}
};
S passthrough(S s) { return s; } // érték szerinti paraméter: implicit move (C++11 óta)
int main() {
S b = passthrough(S{}); // az argumentum inicializálása elided
std::printf("copies=%d moves=%d\n", b.copies, b.moves);
}Buildjegyzőkönyv (lab-gcc profil). A
kimenet -std=c++11, -std=c++17,
-std=c++20 és -std=c++23 módban egyaránt
copies=0 moves=1: a paraméter mindegyik módban mozog,
másolás egyikben sincs. A mérési eljárás általános: azonos forrás, két
-std mód; ha a kimenet eltér, a különbséget dokumentáljuk.
A valódi C++23-as törést (fordul / nem fordul) a Érték, élettartam és kifejezések: auto(x), implicit
move és range-for (40.5) mutatja be.
A range-for élettartam-szigorítása (C++23) a
for (auto e : kif().tag()) alakot érinti: korábban csak a
legfelső temporary élettartama hosszabbodott meg, a köztes temporary a
deklaráció végén elpusztult, és a ciklusmag dangling referencián futott
– azaz nem definiált viselkedés. C++23-ban a range-init minden
temporaryje a ciklus végéig él. Csendes változás: a korábban UB-t
produkáló kód most definiáltan működik, és a destruktorok sorrendje is
megváltozik. Migrációs szabály: a korábbi „véletlenül működött” kódot
nem szabad megtartani mintaértékűnek; a helyes kód tulajdonost és
élettartamot nevez meg, és nem épít a temporary pusztulásának
pillanatára.
A C++23 szigorítása miatt a különböző típusú string literalek
összefűzése – például L"id: " "42" –
ill-formed lett; korábban feltételesen támogatott,
implementációfüggő jelentésű konstrukció volt. Ez jó hír: fordítási
hiba, nem csendes viselkedésváltozás. A tipikus örökség a
_T()/TCHAR makróvilág és a kézzel vegyített
char/wchar_t üzenetek. A javítás: egységes
literalprefix (a forrás és a feldolgozó lánc egy kódolásán), vagy
explicit konverziós függvény a határon; a u8 prefixet
használó kódnál figyelni kell, hogy a u8 literal típusa
char8_t tömb, nem char.
| Ellenőrzés | Keresési minta | Eszköz | Következmény, ha talál |
|---|---|---|---|
| GC-felület | declare_reachable, declare_no_pointers,
pointer_safety |
C++23 build, grep | hard error; hívások törlése |
| Igazított tároló | aligned_storage, aligned_union |
C++23 build + -Wdeprecated |
elavulási diagnosztika;
alignas/optional |
| Denormális lekérdezés | has_denorm, denorm_style |
grep + -Wdeprecated |
elavulás; denorm_min()/platformdok. |
| Visszaadott paraméter | return + by-value paraméter |
kétmódú teszt | másolás→mozgatás; dokumentált eltérés |
| Range-init temporary | for (... : kif().tag()) |
kétmódú teszt, TSan | UB megszűnése, destruktorsorrend |
| Literál-összefűzés | L"/u"/u8" +
" |
C++23 build | hard error; egységes prefix |
A CI-átállítás fokozatokban történjen. 1. fokozat:
minden target fordul -std=c++20 és -std=c++23
módban is, de a C++20-as build marad a kötelező kapu, a C++23-as csak
jelző (warning, nem hiba). 2. fokozat: az új kódra és
az új targetekre -Werror=deprecated-declarations; az
örökölt kód külön targeten, target-szintű
target_compile_options-szal kapja a szigorítást, hogy a
figyelmeztetés ne globális -Wno-...-tal tűnjön el.
3. fokozat: a kétmódú tesztek kimenet-összehasonlítása,
eltérés esetén dokumentált döntés; majd a C++23 lesz az alapértelmezett,
a C++20 pedig ideiglenes visszaállási út. Az elavulási figyelmeztetést
soha ne kapcsold ki projekt-szinten: a lokális, indokolt elnyomás (egy
blokkra szűkített diagnosztika) megőrzi a jelzést a következő migrációs
hullámra.
-Werror-t tenni globálisan.
A könyvtári implementáció nem köteles diagnosztizálni, és a diagnosztika
makróval kikapcsolható; a globális -Werror a
könyvtárfejlesztés miatt is elhasal.std::declare_reachable, (b)
std::aligned_storage, (c) return param; egy
by-value paraméternél, (d) L"a" "b".Slot<T> osztályt, amely aligned_storage
nélkül, alignas(T) std::byte pufferen tárol egy
opcionálisan létező T-t. Készíts emplace,
has_value, reset műveletet, és dokumentáld az
owner–tároló–nézet viszonyt, valamint az invalidáló műveleteket.passthrough(S{}) példára, és egészítsd ki a CI-t úgy, hogy
az eltérés ne bukás, hanem dokumentált riport legyen. Add meg az
ellenőrzési feltételt is.1. megoldás. (a) eltávolított – a C++23-as build
hard errorral jelzi, ezért a legelső fokozatban javítandó; (b) elavult –
a kód továbbra is fordul, de a diagnosztika implementációfüggő, ezért
feature-/compile-probe és -Wdeprecated kell, nem elég a
build; (c) viselkedésváltozó – mindkét módban fordul, csak a választott
konstruktor más, ezért kétmódú futásidejű összehasonlítás szükséges; (d)
ill-formed lett – azaz az eltávolításhoz hasonlóan hard error, de nem
könyvtári, hanem nyelvi szigorítás; a keresés a vegyes literalprefixekre
irányul. Indoklás: a kategória határozza meg a kapu típusát
(build, diagnosztika, futásidejű diff), és ezt a CI-fokozatokhoz kell
rendelni.
2. megoldás. A váz:
// build: g++ -std=c++23 -Wall -Wextra -pedantic -o slot slot.cpp
#include <cstddef>
#include <new>
#include <utility>
template <class T>
class Slot {
public:
Slot() = default;
~Slot() { reset(); }
Slot(const Slot&) = delete;
Slot& operator=(const Slot&) = delete;
template <class... Args>
T& emplace(Args&&... args) {
reset();
::new (static_cast<void*>(buf_)) T(std::forward<Args>(args)...);
live_ = true;
return *get();
}
bool has_value() const noexcept { return live_; }
void reset() noexcept {
if (live_) { get()->~T(); live_ = false; }
}
T* get() { return std::launder(reinterpret_cast<T*>(buf_)); }
private:
alignas(T) std::byte buf_[sizeof(T)];
bool live_ = false;
};Owner a Slot, tároló a buf_ és a
live_ jelző, nézet a get() pointer. Invalidál:
reset() és a destruktor; emplace() előbb
resetel, ezért a korábbi pointer érvénytelenné válik. Ellenőrzési
feltétel: nem hívunk destruktort nem létező objektumra, és
get() csak has_value() mellett dereferálható.
Megjegyzés: a T legyen legalább move-konstruálható, és a
Slot maga nem másolható, mert a belső pointer nem vihető át
triviálisan.
3. megoldás. A teszt a fenti
passthrough program két buildje; a CI-ben két CTest-tesz,
probe_cxx20 és probe_cxx23, mindkettő saját
elvárt kimenettel, és egy harmadik, probe_diff, amely a
kettő eltérését riportfájlba írja, de nem bukik el. Ellenőrzési
feltétel: a riport tartalmazza a fordítóverziót, a -std
kapcsolót és a két kimenetet; a kapu csak akkor szigorodik hibára, ha a
laborprofilban dokumentáltuk, hogy a vizsgált elemre nincs
visszamenőleges DR-alkalmazás. Így a migráció akkor is nyomon követhető,
ha a fordító frissítése megváltoztatja a viselkedést.
A C++23 migráció három különböző kaput igényel: a hard errorokat a
C++23-as build adja, az elavulásokat csak compile probe és
implementációfüggő diagnosztika fogja meg, a viselkedésváltozásokat
pedig kizárólag kétmódú, összehasonlító teszt. A garbage collection
felület eltűnt, az
aligned_storage/aligned_union és a
has_denorm elavult, az implicit move és a range-for
temporary élettartama csendesen változik, a vegyes literal-összefűzés
pedig fordítási hibává vált. A fokozatos CI-átállítás kulcsa, hogy a
figyelmeztetés ne tűnjön el globálisan, és hogy minden eltérés
dokumentált, verzióhoz kötött megfigyelés legyen – ne feltevés.