A C++23-ra való migráció ebben a laborban nem a záróprojekt teljes újraírását jelenti, hanem a C++ core: parser, erőforrások és értéktípusok (39.2) és CMake, tesztek és mérési csomag (39.4) során felépített projekt fokozatos átállítását mérhető, visszagörgethető lépésekben. A cél, hogy a projekt kritikus komponenseit C++23-as szabványos eszközökkel váltsuk ki, miközben a C++20-as build továbbra is működik, és a viselkedés nem változik. A migráció során minden új képesség elérhetőségét feature-test makróval és compile probe-bal ellenőrizzük, és minden új eszközhöz fallbacket adunk. A labor eredménye egy kétprofilos build, egy CI-mátrix, egy migrációs jelentés és egy release notes. A fejezet az Elavult, eltávolított és viselkedést váltó elemek C++23-ban (44.3) során rögzített elavulási és eltávolítási listára épül, de nem ismétli meg annak teljes tartalmát.
A migráció első lépése a kiinduló állapot (baseline) és a viselkedési szerződés (behavioral contract) rögzítése. A viselkedési szerződés a projekt azon megfigyelhető tulajdonságainak összessége, amelyekre a felhasználók és a tesztek támaszkodnak: a CLI kimenete, a hibakódok, a protokoll üzenetek, a szolgáltatás életciklusa és a timing. A szerződés nem tartalmazza a belső implementáció részleteit, például azt, hogy egy result típus hogyan tárolja a hibát, vagy hogy egy tároló milyen iterátorokat használ. A migráció során a szerződés nem változhat, kivéve, ha a változás dokumentált, tesztelt és a release notes-ben jelölt.
A baseline rögzítése a következő lépésekből áll. Először a C++20-as buildet le kell fordítani a CMake, tesztek és mérési csomag (39.4) szerinti CMake-profillal, és le kell futtatni a teljes CTest-készletet, beleértve a CTest, negatív tesztek és sanitizer CI (38.4) szerinti negatív teszteket és sanitizeres futtatásokat. Az eredményt rögzíteni kell: hány teszt fut, mennyi a fordítási idő, mekkora a bináris mérete. Másodszor a CLI kimenetét golden fájlokban kell rögzíteni a tipikus bemenetekre, beleértve a hibaeseteket is. Harmadszor a hibakódokat és a hibaüzeneteket kell dokumentálni, mert a felhasználók ezekre támaszkodhatnak. Negyedszer a hálózati protokoll üzeneteit kell rögzíteni, ha a projekt ilyet használ. Ötödször a mérési pontokat kell kijelölni: a parser futásideje, a szolgáltatás válaszideje, a memóriahasználat és a bináris méret. A méréseket a CMake, tesztek és mérési csomag (39.4) mérési csomagjával kell elvégezni, és az eredményeket verziókezelt fájlban kell tárolni.
A baseline rögzítése után a migráció minden lépését ugyanezekkel a tesztekkel és mérésekkel kell ellenőrizni. Ha egy lépés megváltoztatja a viselkedést, vagy nem hoz mérhető javulást, vissza kell vonni. A visszagörgethetőség érdekében minden lépés külön commit legyen, és a build profilok külön könyvtárban legyenek.
A kétprofilos build azt jelenti, hogy ugyanaz a forráskód két nyelvi
módban fordul: C++20 és C++23. A CMake-ben ezt egy opcióval vezéreljük.
A következő példa a projekt gyökerében lévő CMakeLists.txt
releváns részét mutatja. A példa illusztratív, a CMake és a compiler
verzióját a laborprofilban rögzíteni kell.
cmake_minimum_required(VERSION 3.28)
project(diag LANGUAGES CXX)
option(DIAG_USE_CXX23 "Build with C++23" OFF)
add_executable(diag
src/main.cpp
src/parser.cpp
src/service.cpp
)
if(DIAG_USE_CXX23)
target_compile_features(diag PRIVATE cxx_std_23)
else()
target_compile_features(diag PRIVATE cxx_std_20)
endif()
target_compile_options(diag PRIVATE -Wall -Wextra -Wpedantic)A build parancsok a két profilra:
cmake -S . -B build-cxx20 -G Ninja -DDIAG_USE_CXX23=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-cxx20
ctest --test-dir build-cxx20 --output-on-failure
cmake -S . -B build-cxx23 -G Ninja -DDIAG_USE_CXX23=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build-cxx23
ctest --test-dir build-cxx23 --output-on-failureA CI-mátrix két dimenziója a fordító (GCC, Clang) és a nyelvi mód (C++20, C++23). Minden kombinációhoz rögzíteni kell a konkrét compiler- és standard library-verziót, a build parancsot és a teszt parancsot. A mátrix minden elemén futnak a CTest-tesztek, a sanitizeres futtatások és a golden fájlok ellenőrzése. A C++23-as buildben a feature-test makrók alapján kapcsolódnak be a komponensek; a C++20-as buildben a fallbacknek kell fordulnia. A CI-mátrix akkor tekinthető teljesnek, ha a C++20-as és a C++23-as build ugyanazokat a viselkedési teszteket teljesíti.
A migrációt komponensenként végezzük. Minden komponensnél meg kell
határozni a motivációt, a pontos cserét, a fallbacket és a mérési
szempontokat. A javasolt sorrend: std::expected,
std::print, ranges::to,
std::flat_map. A sorrend a kockázat és a fallback költsége
szerint növekszik.
A C++ core: parser, erőforrások és értéktípusok
(39.2) során a projekt hibakezelése egy saját result típusra épült.
A C++23-as std::expected<T, E> szabványos, monadikus
műveleteket (and_then, or_else,
transform) kínál, és jobban illeszkedik a standard library
többi részéhez. A csere nem jelentheti a hívási helyek teljes
újraírását: vezessünk be egy DiagResult aliast, amely a
C++23-as buildben a std::expected-et, a C++20-as buildben a
meglévő típust használja.
// feature.hpp
#pragma once
#if __has_include(<version>)
# include <version>
#endif
#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L
# include <expected>
# define DIAG_HAVE_EXPECTED 1
#else
# define DIAG_HAVE_EXPECTED 0
#endif// result.hpp
#include "feature.hpp"
#include <string>
#if DIAG_HAVE_EXPECTED
template<class T, class E = std::string>
using DiagResult = std::expected<T, E>;
#else
template<class T, class E = std::string>
using DiagResult = diag::legacy_result<T, E>;
#endifA hívási helyeken a DiagResult-ot kell használni. A
monadikus láncokat csak ott érdemes átírni, ahol a
std::expected szemantikája egyértelmű; a régi result típus
eltérő hibakezelése miatt a viselkedést tesztekkel kell ellenőrizni. A
tipikus hiba, hogy a std::expected nem dob kivételt, a régi
result típus viszont igen; a hibakezelést a hívási helyeken
egységesíteni kell. A mérési szempontok: a fordítási idő, a bináris
méret és a hibaút futásideje.
A projekt naplózása jelenleg printf vagy
iostream alapon működik. A C++23-as std::print
típusbiztos, formázott kimenetet ad, és a formázási szintaxisa a
std::format-tal azonos. A fallback a meglévő
printf vagy std::format +
std::cout marad. A naplózó függvényt érdemes egyetlen
interfész mögé rejteni, hogy a hívók ne lássák a különbséget.
// log.hpp
#include "feature.hpp"
#include <string_view>
namespace diag {
void log(std::string_view msg);
}// log.cpp
#include "log.hpp"
#if DIAG_HAVE_PRINT
# include <print>
void diag::log(std::string_view msg) {
std::print("{}\n", msg);
}
#else
# include <cstdio>
void diag::log(std::string_view msg) {
std::printf("%.*s\n", static_cast<int>(msg.size()), msg.data());
}
#endifA tipikus hiba, hogy a std::print nem flush-el
automatikusan minden esetben; a naplózás végén szükség lehet
std::flush-re. A formázási sztringeket egységesen
{} helyőrzőkkel kell írni, és a fallbackben át kell
alakítani %-re, vagy a fallbackben is
std::format-et kell használni, ha elérhető. A mérési
szempontok: a naplózási throughput, a bináris méret és a fordítási
idő.
A projekt több helyen kézi ciklussal gyűjt egy tartományból vektort.
A C++23-as std::ranges::to<std::vector>() tömörebb és
kifejezőbb, és a standard range-ekkel működik. A fallback egy egyszerű
ciklus.
// collect.hpp
#include "feature.hpp"
#include <ranges>
#include <vector>
template<std::ranges::range R>
auto collect(R&& r) {
#if DIAG_HAVE_RANGES_TO
return std::forward<R>(r) | std::ranges::to<std::vector>();
#else
std::vector<std::ranges::range_value_t<R>> out;
for (auto&& e : r) out.push_back(std::forward<decltype(e)>(e));
return out;
#endif
}A tipikus hiba, hogy a bemeneti tartomány nem forward range, és a
ranges::to többször is bejárhatja; ilyenkor a tartományt
előbb materializálni kell, vagy a fallbacket kell használni. A mérési
szempontok: az allokációk száma, a futásidejű teljesítmény és a bináris
méret.
A projekt rendezett asszociatív tárolóként std::map-et
használ. A C++23-as std::flat_map folytonos tárolásban
tartja a kulcs-érték párokat, ami jobb locality-t és kisebb
memóriahasználatot ad, de a beszúrás és a törlés drágább, és az
iterátorok a beszúrás után érvénytelenné válnak. A fallback a
std::map.
| Tulajdonság | std::map |
std::flat_map |
|---|---|---|
| Tárolás | csomópontos | folytonos |
| Keresés | ||
| Beszúrás | ||
| Iterátor stabilitás | beszúrás után is érvényes | beszúrás után érvénytelen |
| Memória | nagyobb | kisebb |
| Locality | gyengébb | jobb |
// map.hpp
#include "feature.hpp"
#include <map>
#if DIAG_HAVE_FLAT_MAP
template<class K, class V>
using DiagMap = std::flat_map<K, V>;
#else
template<class K, class V>
using DiagMap = std::map<K, V>;
#endifA tipikus hiba, hogy a kód iterátort tárol a tárolóban, és a beszúrás
után azt használja; ez std::flat_map esetén dangling
iterátorhoz vezet. A cserét csak olyan helyen szabad elvégezni, ahol a
kód nem támaszkodik az iterátorok stabilitására. A mérési szempontok: a
keresési idő, a beszúrási idő, a memóriahasználat és a bináris
méret.
A feature-test makrók a fordító és a szabványos könyvtár által
definiált makrók, amelyek egy adott nyelvi vagy könyvtári képesség
elérhetőségét jelzik. A __cpp_lib_* makrók a könyvtári
képességeket jelzik, és a <version> headerben érhetők
el. A laborban használt makrók: __cpp_lib_expected,
__cpp_lib_print,
__cpp_lib_ranges_to_container,
__cpp_lib_flat_map. A pontos értékek verziófüggők; a kódban
a szabványos minimális értékre kell összehasonlítani, de az
elérhetőséget a compiler és a standard library dokumentációja szerint
kell ellenőrizni.
A compile probe egy olyan minimális forrás, amelynek sikeres fordítása igazolja egy képesség elérhetőségét. A feature-test makrók mellett akkor hasznos, ha a makró nem megbízható, például részleges implementáció esetén. Például:
// probe_expected.cpp
#include <expected>
int main() {
std::expected<int, int> e = 42;
return e.value();
}A fallback stratégia lényege, hogy a hívók csak a projekt saját absztrakcióját használják, és a feature-test makrók alapján a build válassza ki az implementációt. A fallbacknek ugyanazt a viselkedést kell biztosítania, mint az új implementációnak. A release notes-ben dokumentálni kell, hogy melyik komponens milyen feature-test makrót igényel, mi a fallback, és milyen viselkedésbeli eltérés van. A release notes a migráció visszagörgethetőségének is az alapja.
A migráció mérhető, ezért minden lépés előtt és után mérni kell. A
metrikák: a fordítási idő, a bináris méret, a futásidejű benchmark, a
memóriahasználat és a tesztek száma. A méréshez használható eszközök:
time a fordítási időhöz, size vagy
bloaty a bináris mérethez, /usr/bin/time -v a
memóriahasználathoz, és egy saját benchmark vagy hyperfine
a futásidejű teljesítményhez. A méréseket azonos környezetben, azonos
bemenettel és többszöri futtatással kell végezni; az eredmények
mediánját érdemes rögzíteni.
A mérési terv része a döntési feltétel is. Ha egy komponens cseréje
nem hoz mérhető javulást, vagy rontja a viselkedést, vissza kell vonni.
Például a std::flat_map bevezetését csak akkor érdemes
megtartani, ha a keresési idő javul, a beszúrási idő nem romlik
jelentősen, és a kód nem támaszkodik az iterátorok stabilitására. A
mérések eredményét a migrációs jelentésben kell dokumentálni. A labor
során a tanuló végzi a méréseket; a fejezet nem állítja, hogy a példák
mért eredményeket tartalmaznak.
Az első hiba, hogy a feature-test makrót a header include előtt
ellenőrizzük. A helyes sorrend: #include <version>,
majd a makró ellenőrzése, majd a szükséges header includolása. A második
hiba, hogy a std::print nem flush-el, és a napló
késleltetve jelenik meg. A javítás: a naplózó függvény végén
std::flush vagy a kimenet lezárása. A harmadik hiba, hogy a
std::flat_map iterátorait a beszúrás után is használjuk; a
javítás: az iterátorokat a beszúrás után újra kell szerezni, vagy
std::map-et kell használni. A negyedik hiba, hogy a
ranges::to többször bejárja a bemeneti tartományt; a
javítás: a tartományt előbb materializálni kell, vagy a fallbacket kell
használni. Az ötödik hiba, hogy a C++20-as buildben a fallback hiányzik,
és a build nem fordul; a javítás: minden új képességhez kötelező a
fallback. A hatodik hiba, hogy a CMake verziója nem ismeri a
cxx_std_23 feature-t; a javítás: a laborprofilban rögzített
CMake-verziót kell használni, vagy a CXX_STANDARD
tulajdonságot kell beállítani, ha az támogatott.
Készíts döntési táblázatot a négy komponens
(std::expected, std::print,
ranges::to, std::flat_map) migrációs
sorrendjéről. Szempontok: kockázat, mérési lehetőség, fallback költsége
és függőségek. Indokold a sorrendet.
Írj egy feature detection headert és egy DiagResult
aliast, amely a C++23-as std::expected-et használja, ha
elérhető, különben a meglévő diag::legacy_result-ot. Add
meg a build parancsot mindkét profilra.
A C++20-as build a következő hibát adja:
error: 'expected' is not a member of 'std'. Mi a hiba oka,
és hogyan javítod? Írd le a javítás lépéseit.
Alakítsd át a naplózást std::print-re, és biztosíts
fallbacket. A naplózó függvény szignatúrája
void log(std::string_view). Add meg a kódot és a build
parancsot.
Tervezz egy mérési tervet a std::flat_map
bevezetéséhez. Milyen metrikákat mérsz, milyen eszközökkel, milyen
környezetben, és hogyan döntesz a bevezetésről?
A javasolt sorrend: std::expected,
std::print, ranges::to,
std::flat_map. Az std::expected kockázata a
legkisebb, mert egy alias mögé rejthető, és a fallback a meglévő típus.
A std::print szintén jól elkülöníthető, a fallback egy
függvény. A ranges::to lokális csere, a fallback egy
ciklus. A std::flat_map kockázata a legnagyobb, mert az
iterátorok invalidációja és az API eltérése miatt a kód több helyen is
módosítást igényelhet. Az ellenőrzési feltétel: minden lépés után a
C++20-as és a C++23-as build is fordul, és a viselkedési tesztek
zöldek.
A feature detection header és az alias a fenti
feature.hpp és result.hpp példa szerint épül
fel. A build parancsok:
cmake -S . -B build-cxx20 -G Ninja -DDIAG_USE_CXX23=OFF
cmake --build build-cxx20
cmake -S . -B build-cxx23 -G Ninja -DDIAG_USE_CXX23=ON
cmake --build build-cxx23Az ellenőrzési feltétel: a C++20-as buildben a
DiagResult a diag::legacy_result, a C++23-as
buildben a std::expected típusra oldódik fel, és a tesztek
mindkét profilban zöldek.
A hiba oka, hogy a C++20-as buildben a std::expected nem
elérhető, mert a __cpp_lib_expected makró nem definiált,
vagy a <expected> header nem elérhető. A javítás
lépései: először a feature detection headerben ellenőrizni kell a
__cpp_lib_expected makrót; másodszor a
DiagResult aliast a diag::legacy_result-ra
kell irányítani; harmadszor a hívási helyeken nem szabad közvetlenül
std::expected-et használni, csak a
DiagResult-ot. Az ellenőrzési feltétel: a C++20-as build
fordul, és a hibakezelési tesztek zöldek.
A naplózó függvény a fenti log.hpp és
log.cpp példa szerint épül fel. A build parancsok a második
feladatnál megadottak. Az ellenőrzési feltétel: a napló kimenete mindkét
profilban ugyanaz, és a std::print esetében a kimenet
flush-elődik. A fallback a printf-et használja, amely a
stringet és az új sort kiírja.
A mérési terv: metrikák a keresési idő, a beszúrási idő, az iterációs
idő, a memóriahasználat és a bináris méret. Eszközök: saját benchmark a
műveletekhez, /usr/bin/time -v a memóriahasználathoz,
size a bináris mérethez. A környezet: rögzített CPU,
memória, compiler-verzió és optimalizálási szint. A döntés: ha a
keresési idő javul, a beszúrási idő nem romlik jelentősen, és a kód nem
támaszkodik az iterátorok stabilitására, a std::flat_map
bevezethető; különben a std::map marad. A döntést a release
notes-ben dokumentálni kell.
A migrációs labor a záróprojekt C++23-ra állítását mérhető,
visszagörgethető lépésekben végzi. A kiinduló állapot és a viselkedési
szerződés rögzítése után a kétprofilos build és a CI-mátrix biztosítja,
hogy a C++20-as és a C++23-as build ugyanazt a viselkedést nyújtsa. A
komponensenkénti csere során a saját result típust
std::expected-re, a naplózást std::print-re, a
kézi materializálást ranges::to-ra, a rendezett tárolót
std::flat_map-re cseréljük, minden esetben feature-test
makróval, compile probe-bal és fallbackkal. A teljesítmény- és
kódméret-összehasonlítás alapján döntünk a megtartásról vagy a
visszavonásról. A tipikus hibák elkerülése és a release notes vezetése a
migráció sikerének feltétele.