رفتن به محتوا

std::optional، std::variant و std::expected

از انواع مجموع ‎C++‎17/23 برای نمایش مقادیر قابل‌تهی، اتحادیه‌های ایمن از نظر نوع، و بازگشت مقدار‌یا‌خطا استفاده کنید — و دسته‌های کاملی از باگ‌ها را در زمان کامپایل حذف کنید.

انواع مجموع: غیرقابل نمایش کردن وضعیت‌های نامعتبر

یک نوع مجموع (sum type) می‌تواند در هر لحظه یکی از چند نوع را نگه دارد، و کامپایلر تضمین می‌کند که تمام حالات را مدیریت کنید. ‎C++‎17 مقادیر std::optional و std::variant را معرفی کرد؛ ‎C++‎23 مقدار std::expected را اضافه کرد. این‌ها با هم الگوهای مستعد خطا را جایگزین می‌کنند:

- std::optional جایگزین «بازگرداندن T یا مقدار نگهبان / اشاره‌گر تهی» می‌شود
- std::variant جایگزین «union + enum برچسب» (ایمن از نظر نوع، بدون رفتار تعریف‌نشده) می‌شود
- 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
    }
}

عملیات‌های مونادیک روی 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 — اتحادیه‌های ایمن از نظر نوع

std::variant در هر لحظه دقیقاً یکی از انواع الگوی خود را نگه می‌دارد. برخلاف اتحادیه‌های C، نوع فعال را ردیابی می‌کند و از دسترسی به نوع اشتباه جلوگیری می‌کند. از std::visit با یک بازدیدکننده (visitor) برای مدیریت تمام جایگزین‌ها استفاده کنید.

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++‎ به Result در Rust است. برخلاف استثناها، فراخواننده باید خطا را صراحتاً مدیریت کند. برخلاف کدهای خطا، خطا می‌تواند اطلاعات غنی حمل کند.

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: مقداری که ممکن است وجود نداشته باشد — جستجوها، پارامترهای اختیاری، مقداردهی اولیه تنبل (lazy)
  • std::variant: مقداری که می‌تواند یکی از چند نوع باشد — گره‌های AST، مقادیر JSON، انواع پیام
  • std::expected: محاسبه‌ای که ممکن است شکست بخورد — ورودی/خروجی فایل، تجزیه (parsing)، اعتبارسنجی، هر چیزی که 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. `std::expected<T, E>` چه تفاوتی با پرتاب ساده استثنا دارد؟