A hibakezelés nem attól jó, hogy van benne
try/catch, hanem attól, hogy a hiba
azonosítható, a kontextusa megmarad, és a hívó a szerződésnek
megfelelően tud dönteni. A C++ban ehhez több eszköz áll rendelkezésre: a
kivétel, a std::error_code, a
std::system_error, az std::optional és a
C++23-as std::expected. Ezek nem egymás helyettesítői,
hanem különböző hibaátadási modellek. A választás alapja az, hogy a hiba
várható-e, kell-e hozzá gép által olvasható hibaazonosság, kell-e emberi
kontextus, és a függvény noexcept határon van-e. Az előző
részek közül különösen a noexcept határok és
hibapolicy, valamint a C-s errno és hibaadatok
élettartama ismerete szükséges.
Kivételt akkor érdemes használni, ha a hiba a helyi kontextusban nem kezelhető, ha egy konstruktor nem tud invariánst felállítani, vagy ha a hibát egy központi határon akarjuk egységesen kezelni. Az explicit eredmény akkor természetes, ha a hiba a művelet szerződésének része: egy fájl hiánya, egy parser hibás bemenete, egy hálózati művelet megszakítása vagy egy stream vége. Az explicit eredményt a hívónak ellenőriznie kell; ez nem „gyengébb” hiba, hanem láthatóbb szerződés.
A döntés feltételei: ha a hibát a hívó rendszeresen kezeli, és a hiba
nem tesz érvénytelenné egy nagyobb objektumot, akkor explicit result; ha
a hiba egy invariánst sért, vagy a függvény nem tud értelmes állapotban
visszatérni, akkor kivétel; ha a függvény noexcept, akkor
kivétel nem hagyhatja el, tehát eredményt kell visszaadni. A
noexcept nem azt jelenti, hogy „nincs hiba”, hanem azt,
hogy a hiba más csatornán megy.
A std::error_code egy
(érték, error_category*) pár. A kategória egy
hibatartományt azonosít, a value pedig a konkrét hibát a tartományon
belül. Az error_code olcsón másolható, nem tulajdonol
dinamikus memóriát, és a kategóriának a program teljes futása alatt
élnie kell; ezért saját kategóriát statikus objektumként kell
definiálni. A nulla érték sikert jelent, függetlenül a kategóriától.
A std::error_condition szintén
(érték, error_category*), de jelentése általános,
hordozható feltétel, nem konkrét hiba. Például egy konkrét OS-hibakód
összehasonlítható a „nincs jogosultság” feltétellel. Az
error_code és error_condition összehasonlítása
a kategóriák equivalent logikáján keresztül történik; két
error_code közvetlen összehasonlítása viszont
kategóriaazonosságot is megkövetel. Ezért nem elég csak a nyers értéket
összevetni.
A std::error_category adja a name(),
message(), default_error_condition() és
equivalent() műveleteket. A message() emberi
szöveget adhat, de az error_code maga nem tárol
szövegkontextust. Ha a hívónak tudnia kell, hogy melyik fájl megnyitása
hibázott, azt külön kell hordozni, például egy
std::system_error objektumban vagy egy saját
hibaobjektumban.
A tipikus OS-hibaút:
-1-gyel, és beállítja az
errno-t.errno értékét azonnal el kell menteni, mert bármely
következő könyvtári hívás felülírhatja.std::error_code készül:
std::error_code(saved, std::system_category()).std::errc feltétellel történik,
például ec == std::errc::permission_denied.std::system_error(ec, "open config") készül.A system_category() és a generic_category()
szerepe eltér: a system_category() az operációs rendszer
hibáit képviseli, a generic_category() a hordozható
std::errc feltételeket. POSIX rendszeren a
system_category() értéke jellemzően errno, de
a pontos leképezés implementációfüggő; a target libc dokumentációja és
egy compile/run probe ellenőrzi. A példák itt illusztratívak, nem
futtatott mérések.
#include <errno.h>
#include <stddef.h>
#include <unistd.h>
enum io_status { IO_OK, IO_EOF, IO_ERROR };
enum io_status read_exact(int fd, void *buf, size_t len, size_t *done, int *err) {
unsigned char *p = (unsigned char *)buf;
size_t n = 0;
while (n < len) {
ssize_t r = read(fd, p + n, len - n);
if (r < 0) {
if (errno == EINTR) continue;
*err = errno;
*done = n;
return IO_ERROR;
}
if (r == 0) { *done = n; return IO_EOF; }
n += (size_t)r;
}
*done = n;
return IO_OK;
}Fordítási példa:
gcc -std=c17 -Wall -Wextra -Wpedantic -c read_exact.c. Itt
az EINTR újrapróbálkozás, az IO_EOF külön
kimenet, az IO_ERROR pedig errno-t hordoz. A C
API explicit eredményt ad, de a hiba kontextusát a hívónak kell
kiegészítenie.
A std::system_error olyan kivételtípus, amely egy
std::error_code-ot és egy emberi kontextust hordoz. A
std::runtime_error leszármazottja, code()-tal
visszaadja a hibakódot, what()-ja pedig jellemzően
tartalmazza a hibaüzenetet és a megadott kontextust; a
what() pontos formátuma nem specifikált. Akkor használjuk,
ha a hibát a függvény határán kivételként akarjuk továbbadni, például
egy fájlmegnyitó wrapperben:
#include <cerrno>
#include <fcntl.h>
#include <system_error>
#include <unistd.h>
int open_or_throw(const char *path) {
int fd = ::open(path, O_RDONLY);
if (fd < 0) {
int saved = errno;
throw std::system_error(
std::error_code(saved, std::system_category()),
"open config");
}
return fd;
}A std::system_error nem helyettesíti az
error_code-ot: a hibaazonosságot a code()
hordozza, a kontextust a what() és a konstruktorban átadott
szöveg. A kivételt const std::system_error&
referenciával érdemes elkapni. Ha a függvény noexcept, itt
nem dobhatunk; ilyenkor expected-tel vagy
error_code-dal kell visszatérni. A C++ kivétel C ABI-n való
átengedése tudatosan kívül esik ezen a részen.
A std::optional<T> vagy tartalmaz egy
T értéket, vagy üres. Nem hordoz hibaokot, hibakódot és
kontextust. Akkor használjuk, ha a hiány normális, várható állapot, és a
hívónak nincs szüksége magyarázatra: cache-lookup, opcionális
konfigurációs mező, „van-e ilyen elem” kérdés. Egy parser eredményére
önmagában nem jó, mert nem tudjuk megkülönböztetni az üres bemenetet, a
hibás karaktert és a túlcsordulást.
A value() üres optional esetén
std::bad_optional_access kivételt dob, a
value_or() alapértéket ad. Ha mégis
optional<T>-t használunk, és mellette
error_code-ot adunk vissza, két csatornán keletkezik a
hiba; ez könnyen inkonzisztens lesz. Ilyenkor inkább
expected<T,E> vagy saját result típus kell.
A std::expected<T,E> vagy egy T
értéket, vagy egy E hibát tartalmaz. C++23-as eszköz;
expected<void,E> is létezik. Az E
jellemzően értéktípus: std::error_code, saját domain enum,
vagy strukturált hibaobjektum. Referenciát hibaoldalon ne használjunk,
mert az élettartam- és ownership-problémákat okoz. A
has_value(), value(), error(),
operator* és value_or() a fő műveletek. A
value() hibás oldalon
std::bad_expected_access<E>-t dob, az
operator* és az error() előfeltétele a
megfelelő oldal létezése; rossz oldal elérése nem definiált
viselkedés.
A C++23-as expected monadic műveletei: az
and_then a sikeres értéket tovább láncolja, a
transform a sikeres értéket képezi, az or_else
a hibaágon ad helyreállítási lehetőséget, a transform_error
a hiba típusát alakítja. Ezek használatát feature-test macro és compile
probe ellenőrzi; a választott compiler és standard library támogatása
eltérhet, ezért a könyv C++20 alapján a saját Result
fallbacket is bemutatja. A monadic műveletek hiányában a kézi
if (has_value()) láncolás továbbra is helyes.
#include <charconv>
#include <cstdint>
#include <expected>
#include <string_view>
enum class parse_error { empty, invalid, out_of_range };
std::expected<std::uint64_t, parse_error> parse_u64(std::string_view s) {
if (s.empty()) return std::unexpected(parse_error::empty);
std::uint64_t v{};
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
if (ec == std::errc::invalid_argument) return std::unexpected(parse_error::invalid);
if (ec == std::errc::result_out_of_range) return std::unexpected(parse_error::out_of_range);
if (ptr != s.data() + s.size()) return std::unexpected(parse_error::invalid);
return v;
}Fordítási példa:
g++ -std=c++23 -Wall -Wextra -Wpedantic -c parse_u64.cpp. A
kód a std::from_chars hibáit domain hibává alakítja; így a
hívó nem kap nyers errno-t, hanem a parser szerződésének
megfelelő hibát. A C++20-as fallback lényege egy
std::variant<T,E>-ra épülő Result, amely
has_value(), value() és error()
műveleteket ad; a részletes példa a feladatok megoldásában szerepel.
A hibák összemosása gyakori tervezési hiba. Négy külön kategóriát érdemes megkülönböztetni:
| Kategória | Jelentés | Tipikus reprezentáció | Retry |
|---|---|---|---|
| Domain error | érvénytelen bemenet, szerződésszegés | enum class parse_error, strukturált domain hiba |
nem; a hívó javítsa |
| Cancellation | külső kérésre leállított művelet | kind::cancelled |
csak új kérésre |
| EOF | normál stream-vége | kind::eof, optional, külön státusz |
nem hiba, nem retry |
| Input/OS error | átviteli vagy OS hiba | std::error_code system_category()-vel |
esetfüggő; pl. EINTR újra |
A domain error determinisztikus: ugyanaz a bemenet ugyanazt a hibát
adja, és a hívó a bemenet javításával orvosolhatja. A cancellation nem
feltétlenül hiba: a művelet szabályosan leállt, az erőforrásokat le kell
zárni. Az EOF nem hiba, hanem a stream állapota; nem szabad
system_error-rá alakítani. Az input error ezzel szemben
külső, átmeneti vagy tartós hiba lehet; itt van értelme
error_code-nak és retry-policy-nak.
#include <cerrno>
#include <cstddef>
#include <expected>
#include <system_error>
#include <unistd.h>
struct io_error {
enum class kind { eof, cancelled, system } k;
std::error_code ec{};
};
std::expected<std::size_t, io_error> read_some(int fd, void *buf, std::size_t len) {
for (;;) {
ssize_t r = ::read(fd, buf, len);
if (r < 0) {
if (errno == EINTR) continue;
return std::unexpected(io_error{io_error::kind::system,
std::error_code(errno, std::system_category())});
}
if (r == 0) return std::unexpected(io_error{io_error::kind::eof, {}});
return static_cast<std::size_t>(r);
}
}Fordítás:
g++ -std=c++23 -Wall -Wextra -Wpedantic -c read_some.cpp.
Az io_error itt kimenetkód: az expected
hibacsatornája minden nem értékes kimenetet kezel, de a
kind megőrzi, hogy EOF-ról, cancellationről vagy OS-hibáról
van szó. Ha az EOF tényleg normál, és a hívó nem köteles minden ágat
kezelni, lehet optional vagy külön
read_status; a lényeg, hogy ne váljon
system_error-rá.
errno-t későn olvassuk ki, például egy
std::string vagy std::error_code konstrukció
után; a köztes hívás felülírhatja.generic_category()-t használunk
system_category() helyett, vagy fordítva; a két tartomány
nem azonos.error_code-ot csak a value alapján hasonlítunk
össze, figyelmen kívül hagyva a kategóriát.optional<T>-t használunk ott, ahol a hívónak
tudnia kellene a hiba okát.expected-nél value()-t hívunk úgy, hogy
nem ellenőriztük a has_value()-t; az operator*
rossz oldalon nem definiált.expected hibaoldalán std::string_view-t
tárolunk egy lokális stringre; a hibaobjektum túlélheti a stringet.system_error-ként
kezeljük, és felesleges retry-t indítunk.noexcept függvényből dobunk; ez
std::terminate-hez vezet.A kivétel a nem lokálisan kezelhető, invariánst sértő hibák eszköze;
az explicit result a szerződés részét képező, várható hibáké. A
std::error_code konkrét hibaazonosságot és kategóriát
hordoz, az std::error_condition hordozható összehasonlítást
tesz lehetővé, a std::system_error pedig kivételként
csomagolja ezt kontextussal. Az std::optional csak hiányt
jelez, nem hibaokot. A C++23-as std::expected explicit,
típusos result; C++20-on saját Result fallback szükséges. A
jó hibamodell külön tartja a domain hibát, a cancellationt, az EOF-ot és
az OS/input hibát, és mindegyikhez megfelelő kezelési politikát
rendel.
Válaszd ki a megfelelő visszatérési módot, és indokold: (a)
lookup_cache(key), ahol a hiány normál; (b)
open_socket(host, port), ahol az OS-hiba és az
EINTR lényeges; (c) parse_port(string), ahol
az üres, hibás és túlcsordult bemenet külön kezelendő.
Írj C++20-as Result<T,E> típust
std::variant-ra, majd
parse_port(std::string_view) -> Result<std::uint16_t, parse_error>
függvényt. Kezeld az üres bemenetet, a nem számjegyet, a túlcsordulást
és a záró karaktert. Add meg a buildparancsot és a validációs
feltételeket.
Az alábbi kódrészletekben keresd meg a hibakezelési hibákat, és javítsd őket:
int fd = ::open(path, O_RDONLY);
std::string msg = "open " + std::string(path);
if (fd < 0) throw std::system_error(errno, std::generic_category(), msg);std::expected<int, std::string_view> bad() {
std::string s = "invalid";
return std::unexpected(s);
}std::optional<Value>: a hiány várható, nincs
szükség hibaokra; a hívó has_value()-vel dönt. (b)
std::expected<Socket, std::error_code> vagy a határon
std::system_error: az OS-hiba azonosítható kell legyen, és
a retry-policyhez EINTR/EAGAIN információk
kellenek. (c)
std::expected<std::uint16_t, parse_error>: a
domainhiba típusos, a hívó a bemenetet javíthatja; az
optional itt elveszítené a hibaokot. Validáció: minden
válasznál nevezd meg, hogy a hiba várható-e, kell-e gép által olvasható
kód, és a függvény noexcept-e.#include <charconv>
#include <cstdint>
#include <string_view>
#include <utility>
#include <variant>
enum class parse_error { empty, invalid, out_of_range };
template<class T, class E>
class Result {
std::variant<T, E> v_;
public:
Result(T v) : v_(std::in_place_index<0>, std::move(v)) {}
Result(E e) : v_(std::in_place_index<1>, std::move(e)) {}
bool has_value() const noexcept { return v_.index() == 0; }
explicit operator bool() const noexcept { return has_value(); }
T& value() & { return std::get<0>(v_); }
const T& value() const & { return std::get<0>(v_); }
E& error() & { return std::get<1>(v_); }
const E& error() const & { return std::get<1>(v_); }
};
Result<std::uint16_t, parse_error> parse_port(std::string_view s) {
if (s.empty()) return parse_error::empty;
unsigned v{};
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
if (ec == std::errc::invalid_argument) return parse_error::invalid;
if (ec == std::errc::result_out_of_range) return parse_error::out_of_range;
if (ptr != s.data() + s.size()) return parse_error::invalid;
if (v > 65535u) return parse_error::out_of_range;
return static_cast<std::uint16_t>(v);
}Build:
g++ -std=c++20 -Wall -Wextra -Wpedantic -c result.cpp.
Validáció: parse_port("") hibája empty;
parse_port("80") értéke 80;
parse_port("65536") hibája out_of_range;
parse_port("8x") hibája invalid;
parse_port("-1") hibája invalid. A
Result nem nyújt monadic műveleteket; C++20-on a láncolást
kézi if-fel kell megírni, vagy a projekt saját
and_then/transform segédeket adhat hozzá.
Az első snippetben az errno-t a std::string
konstrukció után olvassuk, miközben az allokáció felülírhatja; a
generic_category() sem az OS-hiba tartománya. Helyes:
int fd = ::open(path, O_RDONLY);
if (fd < 0) {
int saved = errno;
throw std::system_error(
std::error_code(saved, std::system_category()),
std::string("open ") + path);
}A második snippetben a std::string_view egy lokális
std::string-re mutat, amely a függvény visszatérésekor
megszűnik; a hibaobjektum dangling nézetet hordozna. Javítás: az
E legyen std::string, vagy statikus literálra
mutató const char*, vagy domain enum. Validáció: a hibát a
keletkezés helyén olvassuk ki, a kategória az OS-hibához illeszkedik, és
a hibaobjektum élettartama nem rövidebb a hívó által elvártnál.