Sari la conținut

std::opțional, std::variant și std::expected

Utilizați tipurile de sumă C++17/23 pentru a reprezenta valori nullabile, uniuni sigure de tip și returnări de valoare sau eroare - eliminând clase întregi de erori în timpul compilării.

Tipuri de sumă: face ca statele invalide să nu fie reprezentate

Un tip sumă poate conține unul dintre mai multe tipuri în orice moment, iar compilatorul vă asigură că vă ocupați de toate posibilitățile. C++17 a introdusstd::optionalșistd::variant; C++23 a adăugatstd::expected. Împreună, ele înlocuiesc tiparele predispuse la erori:

-std::optionalînlocuiește „returnTor santinel value / null pointer”
-std::variantînlocuiește „union + tag enum” (în siguranță de tip, fără UB)
-std::expectedînlocuiește „returnTor set error code”

Aceste tipuri mută verificarea erorilor de la disciplina de rulare (sperând că programatorul își amintește să verifice) la impunerea în timp de compilare (sistemul de tip forțează gestionarea corectă).

std::optional — Valori nullabile fără pointeri

std::optionaldeține fie o valoareT, fie nimic (std::nullopt). Acesta înlocuiește antimodelul de utilizare a valorilor sentinelă speciale (-1,"",nullptr) pentru a însemna „nicio valoare”.

optional_demo.cpp
#include <optional>
#include <string>
#include <map>
#include <iostream>

class UserDatabase {
public:
    void add(int id, const std::string& name) {
        users_[id] = name;
    }

    // Returns the user name, or std::nullopt if not found
    std::optional<std::string> find_user(int id) const {
        auto it = users_.find(id);
        if (it == users_.end()) {
            return std::nullopt;  // explicit "no value"
        }
        return it->second;  // implicit conversion to optional
    }

private:
    std::map<int, std::string> users_;
};

int main() {
    UserDatabase db;
    db.add(1, "Alice");
    db.add(2, "Bob");

    // Method 1: Check with has_value()
    auto result = db.find_user(3);
    if (result.has_value()) {
        std::cout << "Found: " << result.value() << '\n';
    } else {
        std::cout << "User not found\n";
    }

    // Method 2: value_or() with a default
    std::cout << db.find_user(1).value_or("<unknown>") << '\n';

    // Method 3: Boolean conversion (idiomatic)
    if (auto user = db.find_user(2)) {
        std::cout << "Found: " << *user << '\n';  // dereference like a pointer
    }
}

Operații monadice pe std::opțional (C++23)

C++23 adaugăand_then,transformșior_elselastd::optional, permițând înlănțuirea în stil funcțional fără instrucțiuniifimbricate.

monadic_optional.cpp
#include <optional>
#include <string>
#include <charconv>
#include <iostream>

// C++23 monadic operations on std::optional

std::optional<std::string> get_env(const std::string& name) {
    const char* val = std::getenv(name.c_str());
    if (!val) return std::nullopt;
    return std::string(val);
}

std::optional<int> parse_int(const std::string& s) {
    int value{};
    auto [ptr, ec] = std::from_chars(
        s.data(), s.data() + s.size(), value);
    if (ec != std::errc{}) return std::nullopt;
    return value;
}

std::optional<int> validated_port(int port) {
    if (port > 0 && port < 65536) return port;
    return std::nullopt;
}

int main() {
    // Without monadic ops: nested ifs
    // With monadic ops (C++23): clean pipeline
    int port = get_env("PORT")
        .and_then(parse_int)        // std::optional<int>
        .and_then(validated_port)   // std::optional<int>
        .or_else([]() -> std::optional<int> {
            std::cerr << "Using default port\n";
            return 8080;
        })
        .value();

    std::cout << "Port: " << port << '\n';
}

std::variant — Uniuni sigure de tip

std::variantdeține exact unul dintre tipurile de șabloane în orice moment. Spre deosebire de uniunile C, urmărește ce tip este activ și împiedică accesarea celui greșit. Utilizațistd::visitcu un vizitator pentru a gestiona toate alternativele.

variant_json.cpp
#include <variant>
#include <string>
#include <vector>
#include <iostream>
#include <cmath>

// A JSON-like value type using variant
struct JsonNull {};

using JsonValue = std::variant<
    JsonNull,                           // null
    bool,                               // true/false
    double,                             // number
    std::string,                        // string
    std::vector<struct JsonNode>        // array
>;

struct JsonNode {
    JsonValue value;
};

// Overloaded visitor pattern (C++17 trick)
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

std::string to_string(const JsonValue& val) {
    return std::visit(overloaded{
        [](JsonNull) -> std::string { return "null"; },
        [](bool b)   -> std::string { return b ? "true" : "false"; },
        [](double d) -> std::string { return std::to_string(d); },
        [](const std::string& s) -> std::string {
            return "\"" + s + "\"";
        },
        [](const std::vector<JsonNode>& arr) -> std::string {
            std::string result = "[";
            for (std::size_t i = 0; i < arr.size(); ++i) {
                if (i > 0) result += ", ";
                result += to_string(arr[i].value);
            }
            return result + "]";
        }
    }, val);
}

int main() {
    JsonValue v1 = 3.14;
    JsonValue v2 = std::string("hello");
    JsonValue v3 = JsonNull{};

    std::cout << to_string(v1) << '\n';  // 3.140000
    std::cout << to_string(v2) << '\n';  // "hello"
    std::cout << to_string(v3) << '\n';  // null

    // std::holds_alternative and std::get
    if (std::holds_alternative<double>(v1)) {
        std::cout << "v1 is double: " << std::get<double>(v1) << '\n';
    }

    // std::get throws std::bad_variant_access if wrong type
    // std::get_if returns nullptr instead — safer
    if (auto* ptr = std::get_if<std::string>(&v2)) {
        std::cout << "v2 is string: " << *ptr << '\n';
    }
}

std::expected — Valoare sau eroare (C++23)

std::expecteddeține fie o valoare de tipT, fie o eroare de tipE. Este răspunsul lui C++ laResultde la Rust. Spre deosebire de excepții, apelantul trebuie să gestioneze eroarea în mod explicit. Spre deosebire de codurile de eroare, eroarea poate conține informații bogate.

expected_demo.cpp
#include <expected>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <system_error>

enum class ParseError {
    FileNotFound,
    InvalidFormat,
    ValueOutOfRange
};

std::string to_string(ParseError e) {
    switch (e) {
        case ParseError::FileNotFound:   return "file not found";
        case ParseError::InvalidFormat:  return "invalid format";
        case ParseError::ValueOutOfRange: return "value out of range";
    }
    return "unknown";
}

std::expected<std::string, ParseError> read_file(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        return std::unexpected(ParseError::FileNotFound);
    }
    std::ostringstream ss;
    ss << file.rdbuf();
    return ss.str();  // implicitly wraps in expected
}

std::expected<int, ParseError> parse_port(const std::string& content) {
    try {
        int port = std::stoi(content);
        if (port < 1 || port > 65535) {
            return std::unexpected(ParseError::ValueOutOfRange);
        }
        return port;
    } catch (...) {
        return std::unexpected(ParseError::InvalidFormat);
    }
}

int main() {
    // Monadic chaining (C++23)
    auto result = read_file("port.conf")
        .and_then(parse_port)
        .or_else([](ParseError e) -> std::expected<int, ParseError> {
            std::cerr << "Error: " << to_string(e)
                      << ", using default\n";
            return 8080;  // fallback
        });

    std::cout << "Port: " << result.value() << '\n';
}
Capcană

Aceste tipuri sunt puternice, dar au margini ascuțite dacă sunt utilizate greșit.

  • Apelarea.value()pe unstd::optionalgol aruncăstd::bad_optional_access— verificați întotdeauna mai întâi sau utilizațivalue_or()
  • std::get(variant)pe tipul greșit aruncăstd::bad_variant_access- preferstd::visitsaustd::get_if
  • std::variantcu tipuri duplicate (de exemplu,variant) este valid, dar confuz - utilizați numai accesul bazat pe index
  • Ignorarea erorii dinstd::expected apelând întotdeauna.value()înfrânge scopul - verificați.has_value()sau utilizați operațiuni monadice
  • std::optionalnu există în standard (încă) - utilizațistd::optional>ca o soluție
Bună practică

Alegeți tipul de sumă potrivit pentru fiecare situație.

  • std::optional: o valoare care ar putea să nu existe — căutări, parametri opționali, inițializare leneșă
  • std::variant: o valoare care ar putea fi unul dintre mai multe tipuri - noduri AST, valori JSON, tipuri de mesaje
  • std::expected: un calcul care ar putea eșua - I/O fișier, analizare, validare, orice returneazăTsau o eroare
Idei cheie
  • std::optionalînlocuiește valorile santinelă și pointerii nuli cu o „valoare sau nimic” sigură de tip
  • std::variantînlocuiește îmbinările C cu o îmbinare cu etichetă sigură - utilizațistd::visitpentru o manipulare exhaustivă
  • std::expected(C++23) esteResultal lui C++ — valoare sau eroare cu înlănțuire monadică
  • Operațiile monadice C++23 (and_then,transform,or_else) permit conducte curate fără blocuriifimbricate
  • Tipurile de sumă mută verificarea erorilor de la disciplina de rulare la aplicarea la timp de compilare

Test — Verifică-ți cunoștințele

(20 XP)

1. What does`std::optional::value_or(default)`do?

2. Care este avantajul`std::visit`față de`std::get<T>`atunci când lucrați cu`std::variant`?

3. Cum diferă`std::expected<T, E>`de simpla aruncare a unei excepții?