सामग्री पर जाएं

std::optional, std::variant और std::expected

नलेबल (nullable) वैल्यू, टाइप-सेफ़ यूनियन और वैल्यू-या-एरर रिटर्न दर्शाने के लिए C++17/23 सम टाइप (sum types) का उपयोग करें — इससे बग की पूरी-पूरी श्रेणियाँ कंपाइल टाइम पर ही खत्म हो जाती हैं।

सम टाइप: अमान्य स्थितियों को व्यक्त करना ही असंभव बनाना

एक सम टाइप (sum type) किसी भी समय कई टाइप में से कोई एक रख सकता है, और कंपाइलर सुनिश्चित करता है कि आप सभी संभावनाओं को हैंडल करें। C++17 में std::optional और std::variant आए; C++23 में std::expected जुड़ा। ये मिलकर एरर-प्रवण पैटर्न की जगह लेते हैं:

- std::optional “T या सेंटिनल वैल्यू / नल पॉइंटर लौटाओ” की जगह लेता है
- std::variant “यूनियन + टैग enum” की जगह लेता है (टाइप-सेफ़, कोई UB नहीं)
- std::expected “T लौटाओ या एरर कोड सेट करो” की जगह लेता है

ये टाइप एरर की जाँच को रनटाइम अनुशासन (यह उम्मीद करना कि प्रोग्रामर जाँचना याद रखेगा) से कंपाइल-टाइम एनफ़ोर्समेंट (टाइप सिस्टम सही हैंडलिंग के लिए बाध्य करता है) की ओर ले जाते हैं।

std::optional — पॉइंटर के बिना नलेबल (nullable) वैल्यू

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 std::optional में and_then, transform और or_else जोड़ता है, जिससे नेस्टेड 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 का उपयोग करें।

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 टाइप की एरर। यह Rust के Result का C++ का उत्तर है। एक्सेप्शन के विपरीत, कॉलर को एरर अवश्य स्पष्ट रूप से हैंडल करनी होती है। एरर कोड के विपरीत, एरर विस्तृत जानकारी साथ ले जा सकती है।

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';
}
सामान्य गलती

ये टाइप शक्तिशाली हैं, लेकिन गलत उपयोग करने पर इनके कुछ जोखिम भरे पहलू नुकसान पहुँचा सकते हैं।

  • खाली std::optional पर .value() कॉल करने से 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 C++23 तक स्टैंडर्ड में मौजूद नहीं है (C++26 में इसे जोड़ा गया है, पर आपके कंपाइलर/लाइब्रेरी में अभी उपलब्ध न हो तो) — विकल्प के रूप में std::optional> का उपयोग करें
सर्वोत्तम अभ्यास

हर स्थिति के लिए सही सम टाइप चुनें।

  • std::optional: ऐसी वैल्यू जो शायद मौजूद न हो — लुकअप, वैकल्पिक पैरामीटर, लेज़ी इनिशियलाइज़ेशन
  • std::variant: ऐसी वैल्यू जो कई टाइप में से कोई एक हो सकती है — AST नोड, JSON वैल्यू, मैसेज टाइप
  • std::expected: ऐसी गणना जो विफल हो सकती है — फ़ाइल I/O, पार्सिंग, वैलिडेशन, कुछ भी जो T या एरर लौटाता है
मुख्य बातें
  • std::optional सेंटिनल वैल्यू और नल पॉइंटर की जगह टाइप-सेफ़ “वैल्यू या कुछ नहीं” देता है
  • std::variant C यूनियन की जगह टाइप-सेफ़ टैग्ड यूनियन देता है — सभी विकल्पों की पूरी हैंडलिंग के लिए std::visit का उपयोग करें
  • std::expected (C++23) C++ का Result है — मोनैडिक चेनिंग के साथ वैल्यू या एरर
  • C++23 के मोनैडिक ऑपरेशन (and_then, transform, or_else) नेस्टेड if ब्लॉक के बिना साफ़ पाइपलाइन बनाने देते हैं
  • सम टाइप एरर की जाँच को रनटाइम अनुशासन से कंपाइल-टाइम एनफ़ोर्समेंट की ओर ले जाते हैं

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. `std::optional::value_or(default)` क्या करता है?

2. `std::variant` के साथ काम करते समय `std::get<T>` की तुलना में `std::visit` का क्या लाभ है?

3. `std::expected<T, E>` सीधे एक्सेप्शन थ्रो करने से कैसे अलग है?