Chuyển đến nội dung

std::tùy chọn, std::variant & std::expected

Sử dụng các loại tổng C++17/23 để biểu thị các giá trị rỗng, kết hợp an toàn loại và trả về giá trị hoặc lỗi - loại bỏ toàn bộ lớp lỗi tại thời gian biên dịch.

Các loại tổng: Làm cho các trạng thái không hợp lệ không thể trình bày được

loại tổng có thể chứa một trong nhiều loại bất kỳ lúc nào và trình biên dịch đảm bảo bạn xử lý được tất cả các khả năng. C++17 đã giới thiệustd::optionalvàstd::variant; C++23 đã thêmstd::expected. Cùng nhau, chúng thay thế các mẫu dễ bị lỗi:

-std::optionalthay thế "trả vềThoặc giá trị trọng điểm/con trỏ null"
-std::variantthay thế "union + tag enum" (loại an toàn, không có UB)
-std::expectedthay thế "trả lạiThoặc đặt mã lỗi"

Các loại này chuyển việc kiểm tra lỗi từ kỷ luật thời gian chạy (hy vọng lập trình viên nhớ kiểm tra) sang thực thi thời gian biên dịch (hệ thống loại buộc phải xử lý chính xác).

std::tùy chọn - Giá trị Nullable không có con trỏ

std::optionalgiữ giá trịThoặc không giữ giá trị nào (std::nullopt). Nó thay thế mô hình phản mẫu bằng cách sử dụng các giá trị trọng điểm đặc biệt (-1,"",nullptr) để có nghĩa là "không có giá trị".

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

Hoạt động đơn âm trên std :: tùy chọn (C++23)

C++23 thêmand_then,transformvàor_elsevàostd::optional, cho phép tạo chuỗi kiểu chức năng mà không cần các câu lệnhiflồng nhau.

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 — Liên minh loại an toàn

std::variantgiữ chính xác một trong các loại mẫu của nó bất kỳ lúc nào. Không giống như liên kết C, nó theo dõi loại nào đang hoạt động và ngăn chặn việc truy cập sai. Sử dụngstd::visitvới khách truy cập để xử lý tất cả các lựa chọn thay thế.

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 — Giá trị hoặc Lỗi (C++23)

std::expectedgiữ giá trị loạiThoặc lỗi loạiE. Đó là câu trả lời của C++ choResultcủa Rust. Không giống như các trường hợp ngoại lệ, người gọi phải xử lý lỗi một cách rõ ràng. Không giống như mã lỗi, lỗi có thể mang nhiều thông tin.

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';
}
Lỗi thường gặp

Những loại này mạnh mẽ nhưng có cạnh sắc nếu sử dụng sai.

  • Gọi.value()trênstd::optionaltrống némstd::bad_optional_access— luôn kiểm tra trước hoặc sử dụngvalue_or()
  • std::get(variant)sai loại némstd::bad_variant_access— ưu tiênstd::visithoặcstd::get_if
  • std::variantcó loại trùng lặp (ví dụ:variant) hợp lệ nhưng gây nhầm lẫn — chỉ sử dụng quyền truy cập dựa trên chỉ mục
  • Bỏ qua lỗi trongstd::expected bằng cách luôn gọi.value()sẽ không đạt được mục đích - kiểm tra.has_value()hoặc sử dụng các thao tác đơn âm
  • std::optionalkhông tồn tại trong tiêu chuẩn (chưa) - sử dụngstd::optional>như một giải pháp thay thế
Thực hành tốt

Chọn loại tổng phù hợp cho từng tình huống.

  • std::optional: Một giá trị có thể không tồn tại — tra cứu, tham số tùy chọn, khởi tạo từng phần
  • std::variant: Giá trị có thể là một trong nhiều loại — nút AST, giá trị JSON, loại thông báo
  • std::expected: Một phép tính có thể thất bại — I/O tệp, phân tích cú pháp, xác thực, bất kỳ thứ gì trả vềThoặc lỗi
Điểm chính
  • std::optionalthay thế các giá trị trọng điểm và con trỏ null bằng "giá trị hoặc không có gì" an toàn kiểu
  • std::variantthay thế các liên kết C bằng liên kết được gắn thẻ an toàn loại - sử dụngstd::visitđể xử lý toàn diện
  • std::expected(C++23) làResultcủa C++ — giá trị hoặc lỗi với chuỗi đơn âm
  • Các hoạt động đơn âm C++23 (and_then,transform,or_else) cho phép các đường ống sạch mà không cần các khốiiflồng nhau
  • Các kiểu tổng chuyển việc kiểm tra lỗi từ kỷ luật thời gian chạy sang thực thi thời gian biên dịch

Câu đố — Kiểm tra kiến thức

(20 XP)

1. `std::optional::value_or(default)`làm gì?

2. Ưu điểm của`std::visit`so với`std::get<T>`khi làm việc với`std::variant`là gì?

3. `std::expected<T, E>`khác với việc đơn giản ném một ngoại lệ như thế nào?