C++17 中 std::optional 的应用实例

在 C++17 之前,我们经常使用指针或特殊值来表示“缺失值”或“可选值”。随着 std::optional 的加入,代码变得更加语义化、类型安全且易于维护。下面通过一个实际案例,展示如何在一个简易的配置解析器中使用 std::optional。

1. 需求场景

我们需要解析一个 JSON 配置文件,其中包含若干可选字段,例如:

{
  "host": "localhost",
  "port": 8080,
  "use_ssl": true,
  "timeout": 30
}
  • hostport 必须出现,否则解析失败。
  • use_ssl 是可选的,缺省为 false
  • timeout 是可选的,缺省为 60 秒。

我们希望在 C++ 代码中以强类型的方式表达这些约束。

2. 设计思路

  1. 使用 std::optional 表示可选字段
    对于 use_ssltimeout,声明为 `std::optional ` 和 `std::optional`。
  2. 提供默认值
    在解析完所有字段后,如果 optional 仍为空,使用业务默认值。
  3. 错误处理
    对于必需字段缺失或类型不匹配,抛出异常或返回错误状态。

3. 示例代码

下面的代码演示了一个极简的解析器实现。为了简化,使用了 nlohmann::json 库来处理 JSON。

#include <iostream>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

// 配置结构体
struct Config {
    std::string host;          // 必需
    int port;                  // 必需
    std::optional <bool> use_ssl;   // 可选
    std::optional <int> timeout;    // 可选
};

// 解析函数
Config parse_config(const std::string& json_str) {
    json j = json::parse(json_str);

    Config cfg;

    // 必需字段
    if (!j.contains("host") || !j["host"].is_string())
        throw std::runtime_error("Missing or invalid 'host'");
    cfg.host = j["host"].get<std::string>();

    if (!j.contains("port") || !j["port"].is_number_integer())
        throw std::runtime_error("Missing or invalid 'port'");
    cfg.port = j["port"].get <int>();

    // 可选字段
    if (j.contains("use_ssl") && j["use_ssl"].is_boolean())
        cfg.use_ssl = j["use_ssl"].get <bool>();
    else
        cfg.use_ssl = std::nullopt;  // 明确标记为空

    if (j.contains("timeout") && j["timeout"].is_number_integer())
        cfg.timeout = j["timeout"].get <int>();
    else
        cfg.timeout = std::nullopt;

    return cfg;
}

// 展示解析结果
void print_config(const Config& cfg) {
    std::cout << "Host: " << cfg.host << '\n';
    std::cout << "Port: " << cfg.port << '\n';

    // 使用 optional 的语义
    if (cfg.use_ssl.has_value())
        std::cout << "Use SSL: " << std::boolalpha << cfg.use_ssl.value() << '\n';
    else
        std::cout << "Use SSL: (default) false\n";

    if (cfg.timeout.has_value())
        std::cout << "Timeout: " << cfg.timeout.value() << "s\n";
    else
        std::cout << "Timeout: (default) 60s\n";
}

int main() {
    std::string raw_json = R"(
    {
        "host": "example.com",
        "port": 443,
        "use_ssl": true
    }
    )";

    try {
        Config cfg = parse_config(raw_json);
        print_config(cfg);
    } catch (const std::exception& e) {
        std::cerr << "解析错误: " << e.what() << '\n';
    }
    return 0;
}

4. 代码说明

  1. std::optional 用法

    • `std::optional use_ssl;`
    • cfg.use_ssl.has_value() 用来检查是否有值。
    • cfg.use_ssl.value() 访问实际值;若为空,调用 value() 会抛出异常。
  2. 默认值的提供
    print_config 中,若 optional 为空,则使用业务默认值 false(SSL)或 60(timeout)。
    也可以在解析完成后立即把默认值填充进去,避免后续每次使用都需要检查。

  3. 异常安全
    使用 try-catch 捕获解析时抛出的异常,保证程序不因错误配置崩溃。

5. 小结

  • std::optional 让“缺失值”成为一种可表达的类型,而不是隐式的 nullptr 或特殊值。
  • 在配置、命令行参数、数据库查询等场景中,使用 optional 能提升代码的可读性和安全性。
  • 结合 C++17 的强类型系统,可以在编译期捕捉更多错误,减少运行时异常。

通过以上示例,你可以在自己的项目中轻松替换掉传统的“缺失值”实现,享受更清晰、更安全的代码体验。

发表评论