Egy szolgáltatás hibamodellje akkor auditálható, ha a hibaáramlás
egyetlen kifejezésláncban olvasható, és minden ponton pontosan tudjuk,
milyen típusú hiba léphet ki belőle. A korábbi error_code, system_error és expected (25.4) részben
az error_code, system_error és
expected alapeseteit rögzítettük; itt azt építjük tovább,
ami egy termelési API-t megkülönböztet egy tankóldától: a rétegzett,
monadikus hibaláncot, a hibaidőtípus rétegenkénti megválasztását és a
kivételhatár szerződését. A Hibakezelési és
exception safety labor (25.5) laborra és a Hibakód, errno és hibaadatok élettartama (11.3)
errno-élettartam-szabályára támaszkodunk.
Források. A szabályok forrása a forrásjegyzék [C++23] tétele ([expected]); a
std::expected maga a P0323, a monadikus függvények a P2505
javaslatból származnak. A fordító- és könyvtártámogatást feature-test
makróval és fordítási próbával döntjük el. A lab-gcc profil
libstdc++-a __cpp_lib_expected-et 202211L értékkel
definiálja, vagyis a monadikus felületet is nyújtja.
expected
alapszerződése és állapotaiA std::expected<T, E> egy tagszerű tárolású
összegtípus: vagy egy T értéket, vagy egy E
hibát tartalmaz, soha nem mindkettőt, és az értéket nem a szabad tárból
allokálja. A std::unexpected<E> csomagoló azért kell,
mert expected<std::error_code, std::error_code>
esetén a puszta return ec; kétértelmű lenne: nem tudni,
értéket vagy hibát adunk-e vissza. A std::unexpected{ec}
egyértelműen a hibaágat jelöli.
Előfeltételek: T nem referencia- és nem függvénytípus,
nem unexpected-specializáció, és lehet void
(expected<void, E> a „siker, de nincs érték” esetre).
E objektumtípus, nem referencia, nem függvénytípus, nem
unexpected-specializáció, és nem void. A
pontos szabványos megfogalmazás helyét a munkatervezetben kell
visszakeresni; itt a szerződés tartalma a lényeg.
Két megfigyelhető állapot van: az objektum vagy értéket, vagy hibát
tárol, és soha nincs egyik nélkül sem (never-empty). A
std::variant-tal ellentétben az expected-nek
nincs „valueless by exception” állapota. Az értékadó műveletek ezt úgy
biztosítják, hogy csak akkor érhetők el, ha a típusok megfelelnek a
követelményeiknek. Az állapotot váltó értékadás (például hibából
értékbe) megköveteli, hogy a T vagy az E
legalább egyike nem dobó módon mozgatható legyen; ennek hiányában az
értékadó operátor nem vesz részt a túlterhelés-feloldásban, és a kód nem
fordul le. Kivétel esetén a művelet a régi állapotot őrzi meg (erős
garancia), nem egy harmadikat.
A hozzáférők szerződése külön-külön:
| Hozzáférő | Előfeltétel | Ha az előfeltétel nem teljesül |
|---|---|---|
has_value(), operator bool |
nincs | — |
operator*, operator-> |
has_value() igaz |
nem definiált viselkedés |
value() |
nincs | hibaállapotban std::bad_expected_access<E>
kivételt dob (az error() másolatával) |
error() |
has_value() hamis |
nem definiált viselkedés |
value_or(v) |
nincs | hibaállapotban a v-t adja |
// expected_states.cpp — C++23: két állapot, value() és a dereferálás
#include <cstdio>
#include <expected>
#include <string>
int main() {
std::expected<int, std::string> ok = 42;
std::expected<int, std::string> bad = std::unexpected(std::string("parse"));
std::printf("ok: has_value=%d *ok=%d value=%d\n", ok.has_value(), *ok, ok.value());
std::printf("bad: has_value=%d error=%s\n", bad.has_value(), bad.error().c_str());
try {
(void)bad.value(); // hibaállapotban: kivétel, nem UB
} catch (const std::bad_expected_access<std::string>& e) {
std::printf("value() dobott: bad_expected_access, error=%s\n", e.error().c_str());
}
bad = 7; // értékadás: hibából értékállapotba
std::printf("bad = 7: has_value=%d\n", bad.has_value());
}Buildjegyzőkönyv (lab-gcc profil,
-std=c++23). A kimenet:
ok: has_value=1 *ok=42 value=42,
bad: has_value=0 error=parse,
value() dobott: bad_expected_access, error=parse,
bad = 7: has_value=1.
Négy művelet alkotja a C++23-as monadikus felületet:
and_then, or_else, transform és
transform_error. Mind a négyet a P2505 adta a P0323-ban
szabványosított std::expected-hez. A két rész feature-test
értéke eltér: __cpp_lib_expected >= 202202L csak az alap
típust jelzi, a monadikus felülethez >= 202211L kell.
Egy korábbi könyvtárkiadás tehát nyújthatja az expected-et
monadikus függvények nélkül; ilyenkor a lánc helyére kézzel írt
if (r) ... else ... elágazás lép.
Legyen F hívható. Ekkor, &-minősítőn
(std::invoke(f, value) szemantikával):
and_then(f): ha van érték, visszatér
f(value)-vel; ha hiba van, az
expected<U, E> hibaágával tér vissza. U
a hívás visszatérési típusa cv- és referencia-minősítők nélkül.
Megkötés: U az expected
specializációja kell legyen, és U::error_type pontosan
E. Ez a kulcs: az and_then nem konvertál
hibát.transform(f): ha van érték,
expected<U, E>-t ad, ahol U a hívás
visszatérési típusa; ha hiba van, változatlanul továbbadja.
U nem lehet unexpected-specializáció, különben
a hiba dupla csomagolásba kerülne.or_else(f): ha hiba van, f(error)-t adja
vissza, amelynek expected<T, G>-nek kell lennie
(G az új hibaidőtípus); ha érték van, változatlanul
továbbad.transform_error(f): ha hiba van,
expected<T, G>-t ad, ahol G az
f(error) visszatérési típusa, érvényes hibaidőtípus; ha
érték van, változatlanul továbbad.expected<void, E> esetén az and_then
és a transform hívhatója nulla paramétert
kap. Ez a leggyakoribb fordítási hibaforrás, amikor egy
expected<T,E>-re írt lambdát
expected<void,E>-re használunk.
A rövidzárlat táblázata: értékágon
and_then/transform fut,
or_else/transform_error nem; hibaágon
fordítva. Értékágon a hiba típusa sosem változik meg
and_then/transform alatt, ezért egy láncon
belül E végig azonos kell legyen — a hibakonverzió helye
kizárólag a transform_error (és az or_else
hibaidőtípus-váltása).
Legyen a : std::expected<int, E>,
f : int → std::expected<double, E>,
g : double → std::string,
h : E → std::error_code,
k : std::error_code → DomainErr.
| Kifejezés | Eredmény típusa | Megjegyzés |
|---|---|---|
a |
expected<int, E> |
kiindulás |
a.and_then(f) |
expected<double, E> |
hiba típusa változatlan |
.transform(g) |
expected<string, E> |
érték transzformálás |
.transform_error(h) |
expected<string, error_code> |
itt vált hibaidőtípust a lánc |
.transform_error(k) |
expected<string, DomainErr> |
rétegváltás a modulhatáron |
a.transform(f) |
expected<expected<double,E>, E> |
hiba: transform nem lapít |
A táblázat tanulsága: a láncot úgy kell felépíteni, hogy a
hibaidőtípus-váltás egyetlen, dokumentált ponton történjen, és a lánc
belsejében and_then-t használjunk, amikor a hívott függvény
maga is expected-et ad.
errno-tól a doménhibáigA példa egy konfigurációbetöltő réteg: a fájlolvasás operációs hibát
(error_code), a feldolgozás doménhibát
(enum class) ad, a kettő találkozását a
transform_error kezeli. Az errno-t minden
esetben azonnal, a következő könyvtárhívás előtt csomagoljuk be — ez a
Hibakód, errno és hibaadatok élettartama (11.3)
szabálya.
// cfg.cpp -- ISO C++23, std::expected monadikus lanc, fallbackkel
#include <version> // a feature-test makrok forrasa: MINDEN teszt elott
#if !defined(__cpp_lib_expected)
# error "A laborprofil standard konyvtara nem nyujt std::expected-et (C++23)"
#endif
// 202211L: monadikus fuggvenyek (P2505); 202202L: csak az alap expected (P0323).
#if __cpp_lib_expected >= 202211L && !defined(CFG_FORCE_FALLBACK)
# define CFG_MONADIC 1
#endif
#include <cerrno>
#include <charconv>
#include <cstdio>
#include <expected>
#include <string>
#include <string_view>
#include <system_error>
namespace cfg {
enum class err { missing, io, malformed, out_of_range };
std::error_code errno_code() noexcept { return {errno, std::system_category()}; }
std::expected<std::string, std::error_code> read_file(const char* path)
{
std::FILE* f = std::fopen(path, "rb");
if (!f) return std::unexpected(errno_code());
std::string out;
char buf[256];
std::size_t n = 0;
while ((n = std::fread(buf, 1, sizeof buf, f)) != 0) out.append(buf, n);
if (std::ferror(f)) {
const std::error_code ec = errno_code(); // meg erzekeny az errno
std::fclose(f);
return std::unexpected(ec);
}
std::fclose(f);
return out;
}
std::expected<std::string_view, err>
field(std::string_view text, std::string_view key) noexcept
{
std::size_t pos = text.find(key);
if (pos == std::string_view::npos) return std::unexpected(err::malformed);
pos += key.size();
std::size_t end = text.find('\n', pos);
if (end == std::string_view::npos) end = text.size();
return text.substr(pos, end - pos);
}
std::expected<int, err> to_int(std::string_view s) noexcept
{
int v{};
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
if (ec != std::errc{} || ptr != s.data() + s.size())
return std::unexpected(err::malformed);
return v;
}
struct config { int workers; };
std::expected<config, err> map_io(std::error_code ec) noexcept
{
return std::unexpected(ec == std::errc::no_such_file_or_directory
? err::missing : err::io);
}
std::expected<int, err> check_range(int v) noexcept
{
if (v < 1 || v > 64) return std::unexpected(err::out_of_range);
return v;
}
#if defined(CFG_MONADIC)
std::expected<config, err> load(const char* path)
{
return read_file(path)
.transform_error([](std::error_code ec) {
return ec == std::errc::no_such_file_or_directory
? err::missing : err::io;
})
.and_then([](std::string&& text) -> std::expected<config, err> {
// a string_view a text-re mutat; a text a lambda vegeig el
return field(text, "workers=")
.and_then(&to_int)
.and_then(&check_range)
.transform([](int v) { return config{v}; });
});
}
#else
// Fallback: ugyanaz a szerzodes kezi elagazasokkal, monadikus fuggvenyek nelkul.
std::expected<config, err> load(const char* path)
{
auto text = read_file(path);
if (!text) return map_io(text.error());
auto f = field(*text, "workers=");
if (!f) return std::unexpected(f.error());
auto v = to_int(*f);
if (!v) return std::unexpected(v.error());
auto r = check_range(*v);
if (!r) return std::unexpected(r.error());
return config{*r};
}
#endif
} // namespace cfg
int main(int argc, char** argv)
{
if (argc != 2) return 2;
if (auto r = cfg::load(argv[1])) std::printf("workers=%d\n", r->workers);
else std::printf("hiba: %d\n", static_cast<int>(r.error()));
return 0;
}Élettartam-térkép: a text std::string
tulajdonosa a load lambdája, a field által
visszaadott string_view kölcsönzött nézet ugyanerre; a
nézet érvényessége a lambda végéig tart, és nem hagyja el a láncot, mert
a végső config csak int-et tartalmaz.
Érvénytelenítő művelet nem történik, mert a text-et a lánc
nem módosítja. Ha a config tagja
std::string_view lenne, a lánc végétől dangling nézetünk
maradna — ez a leggyakoribb tervezési hiba ebben a mintában.
Feature-test és build: a program elején a
<version> után a __cpp_lib_expected
dönt. Ha nincs std::expected, világos #error
áll le; ha van, de 202211L alatti, a load kézi elágazásos
fallbackje fordul; különben a monadikus lánc. A fallback-ág a
-DCFG_FORCE_FALLBACK kapcsolóval monadikus felületet tudó
profilban is lefordítható és tesztelhető.
# rogzitett laborprofilban, peldaul GCC 13+ vagy Clang 17+ mellett ertelmezheto
g++ -std=c++23 -O2 -Wall -Wextra -Wpedantic -o cfg cfg.cpp
# vagy
clang++ -std=c++23 -O2 -Wall -Wextra -Wpedantic -o cfg cfg.cpp
# a fallback-ág tesztje:
g++ -std=c++23 -O2 -Wall -Wextra -Wpedantic -DCFG_FORCE_FALLBACK -o cfg cfg.cppBuildjegyzőkönyv (lab-gcc profil). A
monadikus és a fallback-ág is figyelmeztetés nélkül fordult, és azonosan
viselkedett: workers=8 fájlra workers=8;
workers=99-re hiba: 3 (out_of_range);
workers=abc-re hiba: 2 (malformed); hiányzó
fájlra hiba: 0 (missing). -std=c++20 mellett a
fordítás a saját #error üzenettel állt le, nem
template-hibával. A Clang-sort nem ellenőriztük.
error_code vagy doménstructHárom reális választás van. Az enum class olcsó,
másolható, kimerítő switch-cél; akkor jó, ha a hiba
fajtája elég, a kontextus nem. A std::error_code a
rendszerhatáron kötelező: két szó, olcsón másolható, kategóriával együtt
hordozza a hibát, de üzleti jelentést nem ad. A doménstruct
(struct parse_error { err kind; std::size_t offset; })
akkor kell, ha a hiba helye vagy a kontextus a diagnosztikához
nélkülözhetetlen; ára a méret és a másolási költség.
Ajánlott minta: a modul belsejében error_code, a
publikus API határán transform_error-ral domén típusra
váltás, és a láncon belül E soha nem változik. Az
error_code és a domén típus közötti oda-vissza konvertálást
ne építsd a lánc közepére: minden váltás újabb hely, ahol a
hibainformáció elveszhet.
noexceptA szerződés two-way: expected a számított,
rutinszerű hibákra való (hiányzó fájl, formátumhiba, megtelt puffer),
kivétel a szerződésszegésre és a nem helyreállítható állapotra.
Egy noexcept függvény nem engedheti ki a kivételt, ezért
minden meghiúsulási módját a visszatérési típusban kell kifejeznie — a
noexcept és az expected tehát természetes
páros.
Konstruktorhiba esetén a konstruktor nem adhat vissza
expected-et. Klasszikus minta: privát konstruktor és
publikus static std::expected<T, E> create(...)
gyárfüggvény, amely a validációt elvégzi, és csak érvényes objektumot
konstruál. Ha a konstruktor mégis dob, a hibát a hívó modulhatáron
try/catch-csel alakítsd
expected-dé, és ott rögzítsd, mely kivételeket fordítasz
hibakóddá.
Figyelj rá, hogy a value() szándékosan átlépi a határt:
bad_expected_access<E>-t dob. Ez hasznos a
„lehetetlen” ág jelölésére, de tilos noexcept függvényben
és tilos egy lánc közepén, mert a lánc értékre optimalizált. A monadikus
műveletek maguk sem noexcept-ek, csak ha a hívott
F az; egy noexcept lánchoz minden lépésnek
noexcept-nek kell lennie.
Az expected<T,E> mérete legalább
,
ehhez jön a diszkrimináns (jellemzően egy szó, a padding miatt néha
ingyen), és az igazítás a két típus igazításmaximuma. A képlet mint alsó
korlát értendő: a konkrét érték target ABI és fordítási profil
függvénye, ezért mérés nélkül ne állíts számot. A laborban írj egy
programot, amely kiírja a sizeof-okat, és hasonlítsd össze
a std::variant<T,E> és a kézi
struct { bool ok; T v; E e; } változattal.
Másolás: az expected másolása a benne lévő
T vagy E másolását jelenti. A lánc végén ezért
érdemes std::move(exp).and_then(...)-t használni, mert az
rvalue-minősített hívás T&&-t ad át, és elkerüli a
nagy T felesleges másolását. Kódméret: az
expected jellemzően elágazásokká fordul, míg a
kivételkezelés külön táblákat és unwinder-útvonalat igényel; a két minta
kódméret-összehasonlítását szintén a laborra bízom (size,
nm --size-sort, illetve a kiadott bináris összevetése), nem
állítom mért eredményként.
transform expected-et adó
függvénnyel: expected<expected<T,E>,E>
lesz belőle. Javítás: and_then.E a láncban: az
and_then nem konvertál, fordítási hiba. Javítás: a
hibaidőtípus-váltás transform_error-ral, egyetlen
ponton.error() hívása értékágon, illetve
*r vagy r-> hibaágon: nem
definiált viselkedés. Javítás: előbb has_value(), vagy a
value(), amely hibaágon
std::bad_expected_access-t dob, és nem UB.errno késői olvasása: a köztes
fclose/append felülírhatja. Javítás: azonnali
errno_code() csomagolás.value() egy noexcept
határon: kivétel szökik ki, std::terminate.
Javítás: has_value()-ágazás vagy a hívó
expected-en marad.expected<void,E> hívhatója egy paramétert
vár: fordítási hiba. Javítás: nulla paraméteres lambda.1. feladat (típuslevezetés). Legyen
e : std::expected<int, NetErr>,
f : int → std::expected<Bytes, NetErr>,
g : Bytes → std::size_t,
h : NetErr → std::error_code. Írd fel a
e.and_then(f).transform(g).transform_error(h) kifejezés
pontos típusát, majd add meg, mi lenne a típus, ha a
transform(g) helyett transform(f) állna, és
indokold a különbséget!
2. feladat (kódolás). Írj
write_all(int fd, std::span<const std::byte>)
függvényt, amely
std::expected<std::size_t, std::error_code>-t ad
vissza, EINTR esetén újrapróbál, részleges írást helyesen
kezel, és noexcept. Készíts hozzá egy láncot, amely a hibát
transform_error-ral
enum class io_err { interrupted, no_space, other } típusra
képzi. Add meg a buildparancsot és az ellenőrzési feltételt!
3. feladat (diagnosztika). A következő függvényben legalább három hiba van. Nevezd meg őket, sorrendezd aszerint, hogy fordítási hibát vagy futásidejű hibát okoznak, és írd fel a javított változat láncát!
std::expected<config, cfg::err> load(const char* path) noexcept
{
auto text = cfg::read_file(path); // expected<string, error_code>
return text
.and_then([](const std::string& t) { return cfg::parse_workers(t); })
.transform([](int v) { return config{v}; })
.or_else([](cfg::err e) { return e; })
.value();
}1. megoldás. Lépésenként: e.and_then(f)
típusa std::expected<Bytes, NetErr> (a hiba típusa
változatlan, az érték Bytes). .transform(g)
típusa std::expected<std::size_t, NetErr>.
.transform_error(h) típusa
std::expected<std::size_t, std::error_code>. A hibás
változatban e.transform(f) típusa
std::expected<std::expected<Bytes, NetErr>, NetErr>:
a transform nem lapít, a belső expected
értékként kerül a külsőbe, és a hibainformáció kétszeresen csomagolódik.
Indoklás: a lapítás az and_then szerződése, ezért
expected-et adó hívásra mindig and_then
kell.
2. megoldás.
#include <cerrno>
#include <cstddef>
#include <expected>
#include <span>
#include <system_error>
#include <unistd.h>
enum class io_err { interrupted, no_space, other };
std::expected<std::size_t, std::error_code>
write_all(int fd, std::span<const std::byte> data) noexcept
{
std::size_t done = 0;
while (done < data.size()) {
const ssize_t n = ::write(fd, data.data() + done, data.size() - done);
if (n < 0) {
if (errno == EINTR) continue;
return std::unexpected(std::error_code{errno, std::system_category()});
}
done += static_cast<std::size_t>(n);
}
return done;
}
std::expected<std::size_t, io_err>
write_frame(int fd, std::span<const std::byte> frame) noexcept
{
return write_all(fd, frame).transform_error([](std::error_code ec) {
if (ec == std::errc::no_space_on_device) return io_err::no_space;
if (ec == std::errc::interrupted) return io_err::interrupted;
return io_err::other;
});
}Indoklás: a write_all noexcept, mert minden
hibát a visszatérési típusban fejez ki; az errno-t azonnal
csomagolja, így a ciklus következő iterációja nem írhatja felül. A
done nulla hosszú bemenetre 0-t ad, ami
érvényes siker. Korlát: a ::write size_t
argumentuma SSIZE_MAX felett implementációfüggő; ha ekkora
pufferek előfordulhatnak, a hívást darabolni kell — ez a labor egyik
negatív tesztje. Build:
g++ -std=c++23 -O2 -Wall -Wextra -Wpedantic -c io.cpp.
Ellenőrzési feltétel: zárt fd-re
unexpected, EBADF kategóriával;
EINTR-t szimuláló wrapper mellett a ciklus nem ad vissza
hibát; nulla hosszú spanra 0 érték.
3. megoldás. Elsőként fordítási hibát okoz, hogy a
read_file E = error_code, az
and_then viszont cfg::err hibát adó függvényt
kap; a lánc ezen a ponton nem konvertál. Ugyancsak fordítási hiba, hogy
az or_else cfg::err-t ad vissza
expected helyett. Futásidejű hiba a value(): a
noexcept függvényben
bad_expected_access<cfg::err>-t dobhat, ami
std::terminate-hez vezet, és egyben elveszíti a hibaági
információt. Negyedikként tervezési hiba, hogy text-et
const std::string&-ként veszi át, miközben a lánc
rvalue-ként továbbítja; ez nem hiba, de felesleges másolás nélkül is
elkerülhető. Javított lánc:
read_file(path).transform_error(...), majd
and_then a feldolgozásra, transform a
config-ra, végül return az
expected-en, or_else nélkül.
A std::expected rétegzett használatának három pillére
van: (1) a láncon belül E végig azonos, a váltás
transform_error-ral, egyetlen deklarált ponton történik;
(2) expected-et adó hívásra and_then, sima
értéktranszformációra transform kell, mert csak az előbbi
lapít; (3) a noexcept határ és az expected
ugyanazt a szerződést fejezi ki, ezért a value() sosem
kerülhet egy noexcept láncba. Az errno-t
azonnal csomagold, a doménhibát a modulhatáron alakítsd ki, és a lánc
végén mozgatással dolgozz a felesleges másolás ellen. A C++23-as felület
elérhetőségét feature-test és fordítási próba döntse el, ne a tankönyvi
feltevés.