56 / 80 · C++17 · 约 10 分钟
optional 与 variant:把状态放进类型
optional 表达一个值可能不存在,variant 表达有限候选类型中的一个。二者都管理所含对象的生命周期;安全使用的关键是先确定当前状态,再访问正确分支,而不是用魔法数或裸 union 偷渡状态。
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"; }。这样每个状态都有明确处理逻辑,避免把整数零或空字符串同时当作业务值与缺失标记。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。