انتقل إلى المحتوى

std::اختياري، std::variant & std::متوقع

Use C++17/23 sum types to represent nullable values, type-safe unions, and value-or-error returns — eliminating entire classes of bugs at compile time.

أنواع المجموع: جعل الحالات غير الصالحة غير قابلة للتمثيل

يمكن أن يحتوي نوع المجموع على واحد من عدة أنواع في أي وقت، ويضمن لك المترجم التعامل مع جميع الاحتمالات. قدم C++17std::optionalوstd::variant؛ أضاف C++23std::expected. وهي تحل معًا محل الأنماط المعرضة للخطأ:

- يستبدلstd::optional"إرجاعTأو القيمة الحارسة / المؤشر الفارغ"
- يستبدلstd::variant"union + tag enum" (نوع آمن، بدون UB)
- يستبدلstd::expected"إرجاعTأو تعيين رمز الخطأ"

تقوم هذه الأنواع بنقل التحقق من الأخطاء من نظام وقت التشغيل (على أمل أن يتذكر المبرمج التحقق) إلى فرض وقت الترجمة (يفرض نظام الكتابة المعالجة الصحيحة).

std::اختياري - القيم الخالية بدون مؤشرات

يحمل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
    }
}

العمليات الأحادية على std::اختياري (C++23)

يضيف C++23and_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 — الاتحادات الآمنة من النوع

يحتفظstd::variantبأحد أنواع القوالب الخاصة به بالضبط في أي وقت. على عكس C unions، فإنه يتتبع النوع النشط ويمنع الوصول إلى النوع الخطأ. استخدم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::expected - القيمة أو الخطأ (C++23)

يحملstd::expectedإما قيمة من النوعTأو خطأ من النوعE. إنها إجابة C++ على Rust'sResult. على عكس الاستثناءات، يجب على المتصل ** التعامل مع الخطأ بشكل صريح. على عكس رموز الخطأ، يمكن أن يحمل الخطأ معلومات غنية.

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';
}
خطأ شائع

هذه الأنواع قوية ولكن لها حواف حادة في حالة إساءة استخدامها.

  • الاتصال بـ.value()علىstd::optionalفارغ يؤدي إلى ظهورstd::bad_optional_access- تحقق دائمًا أولاً أو استخدمvalue_or()
  • std::get(variant)من النوع الخطأ يرميstd::bad_variant_access- يفضلstd::visitأوstd::get_if
  • std::variantمع الأنواع المكررة (على سبيل المثال،variant) صالح ولكنه محير - استخدم الوصول المستند إلى الفهرس فقط
  • تجاهل الخطأ فيstd::expected عن طريق الاتصال دائمًا بـ.value()ينفي الغرض - تحقق من.has_value()أو استخدم العمليات الأحادية
  • std::optionalغير موجود في المعيار (حتى الآن) - استخدمstd::optional>كحل بديل
أفضل ممارسة

اختر نوع المبلغ المناسب لكل موقف.

  • std::optional: قيمة قد لا تكون موجودة — عمليات البحث، المعلمات الاختيارية، التهيئة البطيئة
  • std::variant: قيمة يمكن أن تكون واحدة من عدة أنواع — عقد AST، وقيم JSON، وأنواع الرسائل
  • std::expected: عملية حسابية قد تفشل — إدخال/إخراج الملف، التحليل، التحقق من الصحة، أي شيء يُرجعTأو خطأ
النقاط الرئيسية
  • يستبدلstd::optionalالقيم الحارسة والمؤشرات الفارغة بـ "قيمة أو لا شيء" آمنة من النوع.
  • يستبدلstd::variantاتحادات C باتحاد ذي علامة آمنة من النوع - استخدمstd::visitللمعالجة الشاملة
  • std::expected(C++23) هي قيمةResultالخاصة بـ C++ أو خطأ في التسلسل الأحادي
  • تعمل عمليات C++23 الأحادية (and_then، وtransform، وor_else) على تمكين خطوط الأنابيب النظيفة دون كتلifالمتداخلة
  • تقوم أنواع المجموع بنقل التحقق من الأخطاء من نظام وقت التشغيل إلى فرض وقت الترجمة

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ماذا يفعل`std::optional::value_or(default)`؟

2. ما هي ميزة`std::visit`على`std::get<T>`عند العمل مع`std::variant`؟

3. How does`std::expected<T, E>`differ from simply throwing an exception?