A termelési hibajelentés értékét nagyrészt az dönti el, hogy a hiba
pillanatában mennyi kontextust sikerült megőrizni. Egy ritkán
előforduló, nehezen reprodukálható hiba esetén a legfontosabb kérdés nem
az, hogy mi történt, hanem hogy hol: melyik hívási
láncon keresztül jutott a vezérlés a hibás pontra. A C++23
std::stacktrace (P0881) ezt a pillanatképet szabványos,
hordozható formában teszi elérhetővé. Ez a rész a rögzítés szerződését,
a symbolizáció implementációs feltételeit, a hibamodellbe illesztést és
a költség oldalát tárgyalja. A release/debug symbol kérdéskörét itt nem
tanítjuk újra; a release, debug symbol és
reprodukálhatóság rész eredményeire építünk. A hibamodell oldalán a
std::expected monadikus hibaláncai jelentik az
előzményt, a kimenet oldalán pedig az iostream és
pufferelés alapjaira támaszkodunk. A fejezet nem tekinti
ellenőrzöttnek a konkrét fordító- és könyvtártámogatást: minden új
képességet feature-test makróval és compile probe-bal kell igazolni.
A std::basic_stacktrace<Allocator> olyan
értéktípus, amely std::stacktrace_entry elemek sorozatát
tárolja. A std::stacktrace ennek a
std::allocator<std::stacktrace_entry>-ral
paraméterezett aliasza. A tárolt elemek egy rögzítési esemény
pillanatképét írják le: a current() hívásakor a könyvtár
megpróbálja felderíteni a hívási láncot, és a kereteket
stacktrace_entry objektumokként eltárolja. A rögzítés után
a stacktrace független a további végrehajtástól; a hívási lánc későbbi
változása nem hat rá vissza.
A current() három túlterheléssel érhető el:
allokátorral, skip paraméterrel, illetve skip
és max_depth paraméterrel. A skip a legfelső
keretek kihagyására szolgál, a max_depth pedig a rögzítendő
keretek felső korlátja. A skip pontos jelentése
implementációfüggő mértékben eltérhet, ezért a gyakorlatban a
current() hívókeretének kihagyására használjuk, és a
viselkedést a konkrét toolchainen ellenőrizzük. A current()
nem noexcept: a basic_stacktrace allokálhat,
így allokációs hiba esetén std::bad_alloc terjedhet. Ez a
tény a későbbi, noexcept határokra vonatkozó döntéseknél
fontos lesz.
A stacktrace_entry értéktípus, másolható és
összehasonlítható. A description() ember által olvasható
szimbólumnevet ad, a source_file() és
source_line() a forráshelyet, a
native_handle() pedig egy implementációfüggő nyers
azonosítót. A description(), source_file() és
source_line() egyaránt visszaadhat üres értéket, ha a
symbolizációhoz szükséges információ nem elérhető. A
basic_stacktrace size(), empty(),
operator[], at(),
begin()/end() tagfüggvényekkel és
operator<<-val rendelkezik; a
std::to_string túlterhelése teljes szöveges formát ad. A
szöveges forma pontos alakja implementációfüggő, ezért arra nem szabad
gépi parsert építeni.
A következő példa a minimális rögzítést mutatja. A kód nem futtatott, a kimenet formátuma illusztráció.
// st_basic.cpp
#include <stacktrace>
#include <iostream>
void fail() {
const std::stacktrace st = std::stacktrace::current(0, 16);
std::cerr << "hiba helye:\n" << st << '\n';
}
int main() { fail(); }g++ -std=c++23 -g -O1 -fno-omit-frame-pointer st_basic.cpp -lstdc++exp -o st_basic
./st_basicA -lstdc++exp itt feltételes: a GCC/libstdc++ egyes
kiadásaiban a <stacktrace> implementációja a
libbacktrace-re épül, és a linkeléshez ez a kísérleti könyvtár (korábbi
nevén -lstdc++_libbacktrace) szükséges lehet. A pontos
könyvtárnév és annak szükségessége implementáció- és verziófüggő; a
linker hibaüzenete és a toolchain dokumentációja együtt adja a választ.
A libc++ oldalán a C++23 státuszfüggvényt kell ellenőrizni; a támogatás
verziófüggő, és ezt a fejezet nem tekinti igazoltnak.
A használat előtt érdemes compile probe-bal ellenőrizni a képességet:
#include <version>
#if !defined(__cpp_lib_stacktrace)
#error "a toolchain nem hirdeti a std::stacktrace-t"
#endif
#include <stacktrace>
int main() {
auto st = std::stacktrace::current(0, 8);
return st.empty() ? 1 : 0;
}A __cpp_lib_stacktrace ismert értéke a szabványos
<version> fejlécben szerepel; azt nem hardcode-oljuk,
hanem a fejlécből olvassuk ki, és a konkrét értéket a toolchain
verziójához kötjük.
A rögzítés és a symbolizáció két külön lépés. A
current() a keretek azonosítóit rögzíti; a
description(), source_file() és
source_line() hívásakor történhet a szimbolizáció, ami
lényegesen drágább lehet, mint a puszta rögzítés. Ezért a
hibajelentésben érdemes a stacktrace-t nyers formában eltárolni, és csak
a feldolgozás, a megjelenítés vagy a naplózás során szöveggé
alakítani.
A symbolizáció feltételei: a megfelelő bináris, a hozzá tartozó debug
info és a betöltési környezet ismerete. A -g fordítói
kapcsoló debug szekciókat generál; a
-fno-omit-frame-pointer a frame-pointer alapú visszafejtést
teszi megbízhatóbbá optimalizált kód mellett is. A modern disztribúciók
jellemzően PIE binárist linkelnek, így a címek futásidőben az ASLR miatt
változnak. A description() a futó folyamatban a betöltött
modulokból symbolizál; a native_handle() nyers címét
addr2line-nal csak a betöltési bázis ismeretében lehet
feloldani.
A strip --strip-all eltávolítja a
.symtab-ot és a debug szekciókat; a .dynsym
megmarad, de csak az exportált szimbólumokat tartalmazza. Ekkor a
statikus függvények neve és a sorinformáció elveszik, a
description() üres lehet, a source_file() és
source_line() pedig üresen marad. A
strip --strip-debug csak a debug szekciókat távolítja el, a
szimbólumtáblát megtartja. A release buildben is érdemes lehet a debug
infót külön fájlban megőrizni (objcopy --only-keep-debug,
majd --add-gnu-debuglink), és a binárisból eltávolítani; a
build-id (readelf -n) alapján a debug csomag később is
megtalálható.
A stacktrace-t a hiba keletkezésének pontján kell rögzíteni, nem a
hibaút végén. Ha a hibát std::expected-tel adjuk tovább, a
hibaérték tartalmazhatja a stacktrace-t; a monadikus lánc
(and_then, transform, or_else)
ezt az értéket továbbviszi, de a rögzítés helye továbbra is a hiba
keletkezése marad.
// diag_expected.cpp
#include <expected>
#include <stacktrace>
#include <string>
#include <string_view>
#include <charconv>
#include <cstdint>
#include <system_error>
#include <utility>
struct DiagError {
std::string message;
std::stacktrace trace;
};
std::expected<std::uint16_t, DiagError> parse_port(std::string_view s) {
std::uint16_t port{};
const auto* first = s.data();
const auto* last = s.data() + s.size();
auto [ptr, ec] = std::from_chars(first, last, port);
if (ec != std::errc{} || ptr != last)
return std::unexpected(DiagError{
"érvénytelen port: " + std::string(s),
std::stacktrace::current(0, 32)});
return port;
}A skip értéke itt 0, hogy a
parse_port kerete biztosan benne legyen a pillanatképben; a
current() belső implementációs kereteit a könyvtár
eltávolítja. A max_depth = 32 felső korlátot ad a
költségnek.
Kivétel esetén a stacktrace-t a kivétel objektumába építhetjük:
#include <stdexcept>
#include <stacktrace>
#include <string>
#include <utility>
class AppError : public std::runtime_error {
public:
explicit AppError(std::string msg)
: std::runtime_error(msg), trace_(std::stacktrace::current(0, 32)) {}
const std::stacktrace& trace() const noexcept { return trace_; }
private:
std::stacktrace trace_;
};A current() allokálhat, ezért a kivétel konstruktorában
a std::bad_alloc kockázata fennáll; noexcept
destruktorban vagy kivételkezelő rutinban csak akkor hívjuk, ha ezt a
kockázatot kezeljük. A trace_ tagot érdemes mozgatással
átadni, mert a basic_stacktrace másolása a teljes
kerettömböt másolja.
A std::spanstream (P0448) a
<spanstream> fejlécben érhető el; a
std::basic_spanstream<charT, Traits> egy
std::span<charT> által adott fix pufferen dolgozik,
allokáció nélkül. A std::spanbuf a mögöttes puffer, a
span() tagfüggvény kimeneti módban a ténylegesen kiírt
tartományt adja vissza std::span<char>-ként.
(view() tagfüggvény nincs: az a
std::stringstream-é, és std::string_view-t ad;
a két API-t nem szabad felcserélni.) A puffer megtelésekor a stream
badbit vagy failbit állapotba kerül; ezt
ellenőrizni kell, különben csendben csonkolt üzenetet írunk ki.
// diag_line.cpp — C++23: fix pufferes naplósor spanstreammel
#include <array>
#include <cstdio>
#include <span>
#include <spanstream>
#include <string>
#include <string_view>
// Hibapolitika: ha az üzenet nem fér el, a csonkolt sor helyett egy fix,
// rövid jelzés kerül ki; a visszatérési érték jelzi a kiírás sikerét.
bool diag_line(std::string_view what) {
std::array<char, 64> buf{};
std::spanstream ss(buf);
ss << "[diag] " << what << '\n';
if (!ss) {
static constexpr std::string_view overflow =
"[diag] (az uzenet nem fert el a pufferben)\n";
return std::fwrite(overflow.data(), 1, overflow.size(), stderr)
== overflow.size();
}
const std::span<char> written = ss.span(); // a ténylegesen kiírt rész
return std::fwrite(written.data(), 1, written.size(), stderr)
== written.size();
}
int main() {
const bool a = diag_line("short message");
const bool b = diag_line(std::string(200, 'x')); // túl hosszú
return a && b ? 0 : 1;
}A hibapolitika dokumentált: ha az üzenet nem fér el, a csonkolt sor
helyett egy rövid, fix jelzés kerül ki; a függvény visszatérési értéke a
fwrite sikerét jelzi, így a hívó dönthet a naplózási hiba
kezeléséről.
Buildjegyzőkönyv (lab-gcc profil,
g++ -std=c++23 -Wall -Wextra -Wpedantic). A program
figyelmeztetés nélkül fordult; a standard hibakimenetre
[diag] short message, majd a túl hosszú üzenetnél
[diag] (az uzenet nem fert el a pufferben) került, a
kilépési kód 0. A ss.view() hívás
has no member named 'view' hibával nem fordul.
A spanstream előnye a determinisztikus memóriahasználat
és az allokáció elkerülése ott, ahol a naplózás szűkös környezetben
történik. Hátránya, hogy a puffer méretét előre ismerni kell, és a
formázás nem std::format-alapú; a std::format
és a std::print allokálhat, ezért nem helyettesíti a fix
pufferes utat. A spanstream nem async-signal-safe, signal
handlerben nem használható. A feature-test
__cpp_lib_spanstream; a támogatást itt is compile probe-bal
ellenőrizzük.
A rögzítés költsége a keretek számával és a visszafejtési módszerrel
nő; a symbolizáció külön, jellemzően nagyobb költség. A
current() allokálhat, ezért a hot pathban különösen
kerülendő. A védelem eszközei: csak hibaágon rögzíteni; a
max_depth szigorú korlátozása; a skip
beállítása; a symbolizáció késleltetése; rate limit vagy mintavétel ott,
ahol a hiba nagy gyakoriságú. A mérést nem helyettesíti a becslés: a
költséget a saját toolchainen, a saját optimalizálási profilon kell
megmérni.
Egy mérési terv: rögzítsük a max_depth 8, 32 és 128
értékeit; hasonlítsuk össze a -O0 -g, -O2 -g
és -O2 (stripped) buildet; a
-fno-omit-frame-pointer legyen külön változó; a mérést
warmup előzze meg, és a mediánt és a p99-et jegyezzük fel. Az elfogadási
feltétel a szolgáltatás SLO-jából származik, nem univerzális szám.
#include <stacktrace>
#include <chrono>
#include <cstdio>
#include <cstddef>
int main() {
constexpr int N = 10000;
std::size_t total = 0;
const auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < N; ++i) {
const auto st = std::stacktrace::current(0, 32);
total += st.size();
}
const auto t1 = std::chrono::steady_clock::now();
const auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count();
std::printf("N=%d, keretek=%zu, atlag=%.1f ns/hivas\n",
N, total, static_cast<double>(ns) / N);
}A kód nem futtatott; a mérés eredménye a konkrét gépen, fordítón és könyvtáron múlik.
A leggyakoribb hiba a current() hívása minden kérés
kiszolgálásakor, a hot pathban; javítás: csak a hibaágon rögzíteni. A
második, hogy a stacktrace-t a legfelső hibakezelőben rögzítik, ahol már
elveszett a hiba helye; javítás: a hiba keletkezésénél rögzíteni, és az
értéket továbbadni. A harmadik a -lstdc++exp hiánya vagy a
feature-test elmaradása; javítás: compile probe és a linker beállítása.
A negyedik a stripped release bináris, amelyből üres
description() származik; javítás: debug info külön fájlban,
build-id alapján. Az ötödik a basic_stacktrace felesleges
másolása; javítás: mozgatás. A hatodik a spanstream
túlcsordulásának figyelmen kívül hagyása; javítás: a stream állapotának
ellenőrzése. A hetedik a current() hívása
noexcept destruktorban; javítás: a kivétel kockázatának
kezelése vagy elkerülése.
Magyarázó feladat. Készíts döntési táblázatot arról, hogy egy szolgáltatás mely pontjain rögzítenél stacktrace-t (váratlan hiba, validációs hiba, hot path, indulás, leállás, aszinkron callback), és minden sorhoz indokold a döntést a költség és a diagnosztikai érték szempontjából.
Kódolási feladat. Egészítsd ki a
parse_port példát egy with_report függvénnyel,
amely or_else-sel kiegészíti a hibaüzenetet a stacktrace
szöveges formájával, majd írd ki az eredményt. Ügyelj arra, hogy a trace
a hiba keletkezésének helyén rögzüljön.
Diagnosztikai feladat. Egy stripped release
binárisban a description() üres, a
source_file() és source_line() szintén üres.
Sorolj fel legalább három lehetséges okot, és mindegyikhez adj egy
ellenőrzési lépést.
Mérési feladat. Tervezz mérést a
current() költségére: add meg a független változókat, a
mintaszámot, a warmupot, a mért statisztikákat és az elfogadási
feltételt.
1. megoldás. A döntési táblázat lehetséges tartalma:
| Kontextus | Rögzítés | Indok |
|---|---|---|
| Váratlan hiba | igen | a hibaút ritka, a diagnosztikai érték nagy |
| Validációs hiba | nem | nagy gyakoriságú, a hiba helye a hibaüzenetből ismert |
| Hot path | nem | a költség és az allokáció nem fér bele a budgetbe |
| Indulás | opcionális | a kontextus ismert, de a konfigurációs hiba diagnosztizálható |
| Leállás | igen | a leállítás ritka, a hívási lánc fontos |
| Aszinkron callback | igen, rate limittel | a hiba forrása nehezen követhető, de a gyakoriság korlátozandó |
Az indoklás minden sorban a költség és a diagnosztikai érték arányán alapul; az ellenőrzési feltétel az, hogy a hot pathra eső rögzítések száma nulla legyen, és a hibaágon a trace ne legyen üres.
2. megoldás.
std::expected<std::uint16_t, DiagError> with_report(std::string_view s) {
return parse_port(s).or_else(
[](DiagError e) -> std::expected<std::uint16_t, DiagError> {
e.message += "\n--- stacktrace ---\n";
e.message += std::to_string(e.trace);
return std::unexpected(std::move(e));
});
}Az or_else csak a hibaágon hívódik, így a sikeres
útvonalon nincs többletköltség. A trace a parse_port-ban, a
hiba keletkezésénél rögzült; a with_report csak a
megjelenítést egészíti ki. Ellenőrzési feltétel: érvénytelen bemenetre a
message tartalmazza a „stacktrace” jelölőt, és a
trace.size() nullánál nagyobb, feltéve, hogy a toolchain
támogatja a rögzítést.
3. megoldás. Lehetséges okok és ellenőrzések: (a) a
binárist strip --strip-all-lal csökkentették, így a
.symtab és a debug szekciók eltűntek — ellenőrzés:
file, readelf -S; (b) a build nem kapott
-g kapcsolót — ellenőrzés: a buildparancs és a
readelf -S debug szekcióinak hiánya; (c) a debug info külön
fájlban van, de a build-id vagy a debuglink nem található — ellenőrzés:
readelf -n,
readelf --string-dump=.gnu_debuglink; (d) a szimbólumok nem
exportáltak a dinamikus táblába — ellenőrzés: nm -C,
nm -D. A native_handle() nyers címét
addr2line -e és gdb segítségével lehet
feloldani, a PIE betöltési bázisát figyelembe véve.
4. megoldás. Független változók:
max_depth (8, 32, 128), optimalizálási szint
(-O0, -O2), debug info (-g
be/ki), frame pointer (-fno-omit-frame-pointer be/ki).
Mintaszám: legalább 10 000 hívás, 1 000 warmup hívással. Mért
statisztikák: medián, p99, szórás. Elfogadási feltétel: a mérés
variációs koefficiense 5% alatt legyen, és a hibaágon mért rögzítési
költség a szolgáltatás SLO-jából levezetett budgeten belül maradjon. A
mérési kódot úgy kell kialakítani, hogy a fordító ne ejtse ki a hívást
(a keretszám összegzését ki kell írni).
A std::stacktrace a hiba pillanatának hívási láncát
rögzíti; a current() nem noexcept, mert
allokálhat, és a skip/max_depth paraméterekkel
a költség korlátozható. A rögzítés és a symbolizáció külön lépés: a
description(), source_file() és
source_line() implementációfüggően üres lehet, ha a debug
info nem elérhető, a bináris stripped, vagy a PIE betöltési bázisa nem
ismert. A linkelés feltétele implementációfüggő (GCC/libstdc++ esetén
például -lstdc++exp), ezért feature-test és compile probe
szükséges. A stacktrace-t a hiba keletkezésénél kell rögzíteni,
std::expected hibaértékébe vagy kivétel objektumába építve.
A fix pufferes megjelenítésre a std::spanstream
allokációmentes alternatívát ad, de a túlcsordulást ellenőrizni kell. A
hot path védelme a költségmérésen és a hibaágon történő rögzítésen
alapul.