Μετάβαση στο περιεχόμενο

std::προαιρετικό, std::variant & std::αναμενόμενο

Χρησιμοποιήστε τύπους αθροίσματος C++17/23 για να αναπαραστήσετε τιμές μηδενικές, ενώσεις ασφαλείς για τον τύπο και επιστροφές τιμής ή σφάλματος — εξαλείφοντας ολόκληρες κατηγορίες σφαλμάτων τη στιγμή της μεταγλώττισης.

Τύποι αθροίσματος: Καθιστώντας τα μη έγκυρα κράτη μη αντιπροσωπευτικά

Ένας τύπος αθροίσματος μπορεί να χωρέσει έναν από πολλούς τύπους ανά πάσα στιγμή και ο μεταγλωττιστής διασφαλίζει ότι χειρίζεστε όλες τις δυνατότητες. Το C++17 παρουσίασε ταstd::optionalκαιstd::variant. Το C++23 πρόσθεσε τοstd::expected. Μαζί, αντικαθιστούν μοτίβα επιρρεπή σε σφάλματα:

- Τοstd::optionalαντικαθιστά το "επιστροφήTή τιμή φρουρού / μηδενικός δείκτης"
- Τοstd::variantαντικαθιστά το "union + tag enum" (ασφαλές τύπου, χωρίς UB)
- Τοstd::expectedαντικαθιστά το "επιστροφήTή ορισμός κωδικού σφάλματος"

Αυτοί οι τύποι μετακινούν τον έλεγχο σφαλμάτων από πειθαρχία χρόνου εκτέλεσης (ελπίζοντας ότι ο προγραμματιστής θυμάται να ελέγξει) σε επιβολή χρόνου μεταγλώττισης (το σύστημα τύπου επιβάλλει τον σωστό χειρισμό).

std::optional — Μηδενικές τιμές χωρίς δείκτες

Τοstd::optionalέχει είτε τιμήTείτε τίποτα (std::nullopt). Αντικαθιστά το αντίθετο της χρήσης ειδικών τιμών φρουρού (-1,"",nullptr) για να σημαίνει "καμία τιμή".

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
    }
}

Monadic Operations στο std::optional (C++23)

Το C++23 προσθέτει ταand_then,transformκαιor_elseστοstd::optional, επιτρέποντας την αλυσίδα λειτουργικού στυλ χωρίς ένθετες δηλώσειςif.

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 — Type-Safe Unions

Τοstd::variantδιατηρεί ακριβώς έναν από τους τύπους προτύπων του ανά πάσα στιγμή. Σε αντίθεση με τις ενώσεις C, παρακολουθεί ποιος τύπος είναι ενεργός και αποτρέπει την πρόσβαση σε λάθος. Χρησιμοποιήστε τοstd::visitμε έναν επισκέπτη για να χειριστείτε όλες τις εναλλακτικές λύσεις.

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::αναμενόμενο — Τιμή ή Σφάλμα (C++23)

std::expectedholds either a value of typeTor an error of typeE. It is C++'s answer to Rust'sResult. Unlike exceptions, the caller must handle the error explicitly. Unlike error codes, the error can carry rich information.

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';
}
Παγίδα

These types are powerful but have sharp edges if misused.

  • Κλήση.value()σε άδειοstd::optional ρίχνει τοstd::bad_optional_access— ελέγχετε πάντα πρώτα ή χρησιμοποιείτε τοvalue_or()
  • std::get(variant)on the wrong type throwsstd::bad_variant_access— preferstd::visitorstd::get_if
  • std::variantwith duplicate types (e.g.,variant) is valid but confusing — use index-based access only
  • Ignoring the error instd::expected by always calling.value()defeats the purpose — check.has_value()or use monadic operations
  • std::optionaldoes not exist in the standard (yet) — usestd::optional>as a workaround
Βέλτιστη πρακτική

Επιλέξτε τον σωστό τύπο αθροίσματος για κάθε περίπτωση.

  • std::optional: Μια τιμή που μπορεί να μην υπάρχει — αναζητήσεις, προαιρετικές παράμετροι, αργή προετοιμασία
  • std::variant: A value that could be one of several types — AST nodes, JSON values, message types
  • std::expected: Υπολογισμός που ενδέχεται να αποτύχει — I/O αρχείου, ανάλυση, επικύρωση, οτιδήποτε επιστρέφειTή σφάλμα
Βασικά συμπεράσματα
  • Τοstd::optionalαντικαθιστά τις τιμές φρουρού και τους μηδενικούς δείκτες με μια ασφαλή για τον τύπο "τιμή ή τίποτα"
  • Τοstd::variantαντικαθιστά τις ενώσεις C με μια ένωση με ετικέτα ασφαλείας τύπου — χρησιμοποιήστε τοstd::visitγια εξαντλητικό χειρισμό
  • Τοstd::expected(C++23) είναι τοResultτης C++ — τιμή ή σφάλμα με μονοδική αλυσίδα
  • Μοναδικές λειτουργίες C++23 (and_then,transform,or_else) επιτρέπουν καθαρούς αγωγούς χωρίς ένθετα μπλοκif
  • Οι τύποι αθροίσματος μετακινούν τον έλεγχο σφαλμάτων από την πειθαρχία χρόνου εκτέλεσης στην επιβολή χρόνου μεταγλώττισης

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Τι κάνει το`std::optional::value_or(default)`;

2. Ποιο είναι το πλεονέκτημα του`std::visit`έναντι του`std::get<T>`όταν εργάζεστε με το`std::variant`;

3. Σε τι διαφέρει το`std::expected<T, E>`από την απλή εξαίρεση;