C++ / a working model

56 / 80   ·   C++17   ·   约 10 分钟

optional 与 variant:把状态放进类型

先记住这句话

optional 表达一个值可能不存在,variant 表达有限候选类型中的一个。二者都管理所含对象的生命周期;安全使用的关键是先确定当前状态,再访问正确分支,而不是用魔法数或裸 union 偷渡状态。

本篇内容
  1. optional 的空状态不是特殊数值
  2. variant 为封闭类型集合建模
  3. 按领域含义选择容器
  4. 运行示例
  5. 动手练习

optional 的空状态不是特殊数值

C++17 的 optional<T> 包含自己的 T 存储并记录是否有值,不是默认在堆上分配一个 T 的指针。nullopt 表示没有结果;即使 T 是 int,零也可以是完全有效的结果,因此不必挪用 -1 或零作为隐含的失败标志。

访问前用 has_value 或条件判断确认状态。空 optional 调用 value 会抛出 bad_optional_access,直接解引用则不满足前置条件。尤其 optional<bool> 的条件判断检查是否有值,不检查那个 bool 是否为 true;两层状态应分别读取。

variant 为封闭类型集合建模

variant<int, string> 同一时刻通常持有一个候选对象,并跟踪活动类型。get_if 接收 variant 指针,类型不匹配时返回空指针;get 类型不匹配则抛出 bad_variant_access。visit 将访问逻辑应用到当前活动项,编译期要求访问器能处理相关候选组合。

默认构造 variant 并不意味着“空”,而是尝试构造第一个候选类型。需要明确的无内容状态时,可把 monostate 作为候选。某些抛异常的类型切换还可能使其进入 valueless_by_exception 状态,因此不要声称 variant 在任何情况下都永远含有一个值。

按领域含义选择容器

示例把单个数字解析结果建模为 optional,再将一条消息表示为整数或字符串。所有状态分支都在访问前检查,visit 把两种消息统一渲染成 string,而不会把整数当成地址或错误解释存储中的字节。

如果失败原因也重要,optional 往往信息不足,C++23 expected 更贴合“成功值或错误”;如果状态是封闭的一组不同业务事件,variant 更自然。不要用 variant<monostate, T> 机械替代 optional,也不要用可空指针替代独立值,除非接口确实需要借用语义。

容易答错的地方

  • optional<bool>{false} 仍然处于有值状态,if (option) 会进入分支;检查业务真假应在有值后读取 *option。
  • visit 的访问器必须覆盖所有候选类型,并按所用重载满足返回类型要求;只处理当前测试里的那一种不能组成完整接口。

运行一个例子

最低标准 C++17 · 完整程序 · 下载 .cpp

#include <cassert>
#include <iostream>
#include <optional>
#include <string>
#include <variant>

std::optional<int> digit(char c) {
    if (c >= '0' && c <= '9') return c - '0';
    return std::nullopt;
}
struct Render {
    std::string operator()(int n) const { return std::to_string(n); }
    std::string operator()(const std::string& s) const { return s; }
};

int main() {
    auto zero = digit('0');
    assert(zero.has_value() && *zero == 0);
    assert(!digit('x'));
    std::variant<int, std::string> message = 7;
    assert(std::visit(Render{}, message) == "7");
    message = std::string("ready");
    assert(std::get_if<int>(&message) == nullptr);
    assert(std::visit(Render{}, message) == "ready");
    std::cout << *zero << ' ' << std::visit(Render{}, message) << '\n';
}

在本地编译

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread modern-optional-variant.cpp -o example && ./example

预期结果

0 ready

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

若要让 message 显式表示尚未收到消息,怎样改类型与访问器?

查看参考答案

改为 variant<monostate, int, string> 并默认构造,使首个候选 monostate 成为初态。给 Render 增加 string operator()(monostate) const { return "pending"; }。这样每个状态都有明确处理逻辑,避免把整数零或空字符串同时当作业务值与缺失标记。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录