Přeskočit na obsah

Strategie řešení chyb: Výběr správného přístupu

Compare exceptions, error codes, and std::expected — learn when each approach is appropriate and how to handle errors in constructors, destructors, and multithreaded code.

Žádné univerzální řešení

C++ nabízí několik mechanismů zpracování chyb, každý s různými kompromisy. Výběr správného závisí na vaší doméně:

Výjimky jsou nejlepší pro chyby, které jsou vzácné a nelze je zpracovat lokálně. Mají nulové náklady na cestě k úspěchu, ale jsou drahé, když jsou vrženy (1000 $ nanosekund). Automaticky se šíří napříč hovory.

Chybové kódy (návratové hodnoty,errno,std::error_code) jsou nejlepší pro chyby, které jsou očekávané a časté. Mají malé, konstantní náklady na každý hovor (volající musí zkontrolovat návratovou hodnotu). Jsou standardem v C API a kódu na úrovni jádra.

std::expected (C++23) kombinuje to nejlepší z obou: nutí volajícího, aby uznal chybu (jako chybové kódy), ale nese bohatý typ chyby (jako výjimky) a podporuje monadické řetězení. Rychle se stává preferovaným přístupem v novém kódu C++.

Klíčový poznatek: toto nejsou konkurenční filozofie – jsou to nástroje pro různé situace a produkční kód často používá všechny tři.

Porovnání: Výjimky vs kódy chyb vs std::očekávané

| Kritérium | Výjimky | Kódy chyb |std::expected|
|---|---|---|---|
| Náklady na úspěch | Nula (na základě tabulky) | Větev + kontrola | Minimální (uchová hodnota) |
| Cena za chybu | Vysoká (odvíjení) | Stejně jako úspěch | Stejně jako úspěch |
| Lze ignorovat? | Ne (program se ukončí) | Ano (tiché chyby) | Těžší (musí mít přístup.value()) |
| Konstruktéři | Jediná možnost | Nelze vrátit hodnotu | Nelze vrátit hodnotu |
| Destruktory | Nesmí házet | Dobře | Dobře |
| Složitelnost | Omezené (zkusit/chytit) | Manuální if-řetězy | Monadický (a_pak, transformovat) |
| Binární velikost | Větší (odvíjecí stoly) | Minimální | Minimální |
| Bezpečný v reálném čase? | Ne (čas bez omezení) | Ano | Ano |

Zpracování chyb v konstruktorech

Konstruktory nemohou vracet chybové kódy – nemají žádnou návratovou hodnotu. Výjimky jsou jediným čistým mechanismem pro signalizaci poruchy konstrukce. Vyhozená výjimka znamená, že objekt nebyl nikdy plně zkonstruován, takže jeho destruktor není nikdy volán (ale jsou volány destruktory již zkonstruovaných dílčích objektů a základen).

constructor_error.cpp
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>

class ConfigFile {
public:
    explicit ConfigFile(const std::string& path)
        : path_(path), stream_(path) {
        if (!stream_.is_open()) {
            throw std::runtime_error(
                "Cannot open config file: " + path);
        }
        // Parse the file — if this throws, stream_ is
        // already constructed and its destructor will
        // close the file automatically (RAII)
        parse();
    }

    std::string get(const std::string& key) const {
        auto it = data_.find(key);
        if (it == data_.end()) {
            throw std::out_of_range("Key not found: " + key);
        }
        return it->second;
    }

private:
    void parse() {
        std::string line;
        while (std::getline(stream_, line)) {
            auto pos = line.find('=');
            if (pos != std::string::npos) {
                data_[line.substr(0, pos)] = line.substr(pos + 1);
            }
        }
    }

    std::string path_;
    std::ifstream stream_;  // RAII: closed by destructor
    std::map<std::string, std::string> data_;
};

// Usage: the object is fully valid or does not exist
int main() {
    try {
        ConfigFile cfg("/etc/myapp.conf");
        std::cout << cfg.get("port") << '\n';
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
}

std::error_code pro chyby na systémové úrovni

std::error_codeposkytuje odlehčený mechanismus bez házení pro chyby na úrovni systému. Knihovnaje skvělým příkladem — většina funkcí má přetížení, které místo vyvolání přebírá parametrstd::error_code&.

error_code.cpp
#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

// Throwing version — suitable for unexpected errors
void copy_file_throwing(const fs::path& src, const fs::path& dst) {
    fs::copy_file(src, dst);  // throws fs::filesystem_error on failure
}

// Non-throwing version — suitable for expected failures
bool copy_file_safe(const fs::path& src, const fs::path& dst) {
    std::error_code ec;
    fs::copy_file(src, dst, ec);  // sets ec instead of throwing
    if (ec) {
        std::cerr << "Copy failed: " << ec.message()
                  << " (" << ec.category().name()
                  << ':' << ec.value() << ")\n";
        return false;
    }
    return true;
}

int main() {
    // The error_code version is preferable in loops where
    // failure is expected (e.g., batch file processing)
    for (const auto& entry : fs::directory_iterator(".")) {
        std::error_code ec;
        auto sz = fs::file_size(entry.path(), ec);
        if (!ec) {
            std::cout << entry.path().filename()
                      << ": " << sz << " bytes\n";
        }
    }
}
Úskalí

Destruktory v C++11 a novějších jsou implicitněnoexcept. Pokud destruktor hodí, zavolá sestd::terminate(). I když výslovně označíte destruktornoexcept(false), vyvolání během odvíjení zásobníku (když je již aktivní jiná výjimka) volástd::terminate().

  • Zaznamenat a potlačit: Pokud se operace čištění nezdaří, zaprotokolujte chybu a pokračujte
  • Poskytněte samostatnou metoduclose(): Umožněte volajícím explicitně zpracovat chyby čištění před zničením
  • Navrhněte prostředky tak, aby čištění nemohlo selhat: Například zavření deskriptoru souboru je vždy úspěšné na úrovni operačního systému
  • Nikdy nevolejte funkci házení z destruktoru, aniž byste ji zabalili do pokusu/úlovku

Zpracování chyb ve vícevláknovém kódu

Výjimky nepřekračují hranice vláken. Pokud vlákno vyvolá a výjimka není zachycena v tomto vláknu, zavolá sestd::terminate(). Postup šíření výjimek mezi vlákny:

1. Použijte std::future a std::async: Výjimky vyvolané v asynchronní úloze jsou zachyceny a znovu vyvolány, když v budoucnu zavoláte.get().
2. Použijte std::exception_ptr sstd::current_exception()astd::rethrow_exception()k ručnímu zachycení a přenosu výjimek.
3. Použijte std::promise::set_exception() k odeslání výjimky z vlákna producenta spotřebiteli.

Pro souběžný kód s vysokou propustností jsou často preferovány chybové kódy nebostd::expected, protože se vyhnou nákladům na infrastrukturu výjimek v každém vlákně a lze je jednodušeji uvažovat napříč hranicemi vláken.

Osvědčený postup

Přizpůsobte mechanismus zpracování chyb situaci.

  • Používejte výjimky pro chyby, které jsou vzácné, nelze je zpracovat lokálně a měly by se šířit nahoru v zásobníku (např. soubor nenalezen, databáze mimo provoz)
  • Používejte chybové kódy pro vysokofrekvenční operace, kde je selhání normální a očekávané (např. vynechání vyhledávání v mezipaměti, neblokování I/O by blokováno)
  • Použijtestd::expected pro nový kód, kde chcete explicitní zpracování chyb s bohatými typy chyb a monadickým složením
  • Používejte výjimky v konstruktorech – jsou jediným způsobem, jak signalizovat selhání předtím, než objekt existuje
  • Nikdy nevhazujte destruktory – zaznamenejte chybu a potlačte ji
Klíčové poznatky
  • Výjimky, chybové kódy astd::expectedjsou doplňkové nástroje – nikoli konkurenční filozofie
  • Konstruktéři musí používat výjimky pro selhání signálu; chybové kódy nejsou možností
  • Destruktory nikdy nesmí házet – jsou implicitněnoexceptod C++11
  • Výjimky nepřekračují hranice vláken – k jejich přenosu použijtestd::futurenebostd::exception_ptr
  • std::error_codeje ideální pro chyby na úrovni systému, kde chcete API bez vyvolání

Kvíz — Otestujte své znalosti

(15 XP)

1. Proč jsou výjimky preferovaným mechanismem zpracování chyb v konstruktorech?

2. Co se stane, když je v destruktoru vyvolána výjimka během odvíjení zásobníku z jiné výjimky?

3. Jak lze přenášet výjimky mezi vlákny?