词汇类型与错误建模:让接口自己说话 / Vocabulary Types and Error Modeling
词汇类型的意思是:大家看到这个类型,就能立刻理解一部分接口语义。
它们不是炫技容器,而是把“可能没有值”“几种结果之一”“只是观察不拥有”“失败带错误信息”写进类型。
1. 为什么返回特殊值不够好
旧式接口经常这样写:
cpp
int find_user_id(const std::string& name);约定:
text
返回 >= 0 表示找到
返回 -1 表示没找到问题是:
text
调用者可能忘记检查 -1
-1 是否永远不可能是合法值?
接口声明本身看不出这个约定
失败原因无法表达类型更好的做法是:
cpp
#include <optional>
std::optional<int> find_user_id(const std::string& name);这行声明直接告诉你:
text
可能有 int。
也可能没有值。2. optional<T> 表达可能没有值
使用:
cpp
#include <optional>
#include <string_view>
std::optional<int> parse_int(std::string_view text) {
if (text == "42") {
return 42;
}
return std::nullopt;
}调用:
cpp
if (auto value = parse_int("42")) {
int x = *value;
}optional<int> 可以处于两种状态:
text
+---------------------+
| has value |
| int object inside |
+---------------------+
+---------------------+
| no value |
| no int object |
+---------------------+optional 适合表达:
text
查找可能没有结果
解析可能失败但不关心详细原因
缓存值可能还没生成
配置项可能不存在不适合表达:
text
失败原因很重要
需要错误码或错误消息
有多种失败类型需要区分3. variant 表达有限几种状态之一
std::variant 可以保存几种类型之一:
cpp
#include <string>
#include <variant>
using Token = std::variant<int, double, std::string>;它表达:
text
Token 当前要么是 int,
要么是 double,
要么是 string。
不能是别的。访问:
cpp
#include <iostream>
#include <variant>
void print(Token const& token) {
std::visit([](auto const& value) {
std::cout << value << '\n';
}, token);
}std::visit 会根据当前真实替代类型调用 lambda。
variant 适合:
text
词法分析 token
配置值可能是 int/string/bool
状态机里的有限状态
消息类型有限且明确它比 void* 或随便用基类指针更明确。
4. string_view 和 span 表达非拥有观察
std::string_view:
cpp
void print_name(std::string_view name);意思是:
text
我只看一段字符。
我不拥有这段字符。std::span<int>:
cpp
void fill(std::span<int> values);意思是:
text
我看一段连续 int。
我不拥有这段数组。它们通常内部只保存:
text
pointer
length所以生命周期非常重要。
危险例子:
cpp
#include <string>
#include <string_view>
std::string_view title() {
std::string s = "hello";
return s;
}返回的 string_view 悬垂。
因为它观察的 std::string 已经析构。
5. expected 风格结果
optional 只能表达有或没有。
如果你需要错误信息,可以使用 expected 风格。
C++23 有 std::expected。
C++17/20 项目里常见自定义:
cpp
template <class T, class E>
class Result;接口形状:
cpp
Result<User, ParseError> parse_user(std::string_view text);它表达:
text
成功时有 User。
失败时有 ParseError。这比:
cpp
bool parse_user(std::string_view text, User& out, ParseError& error);更容易让调用者看出结果结构。
6. 异常、optional、expected 怎么选
没有一个永远正确的错误模型。
可以先按这个方向选:
text
失败是普通分支,只需要有/无
-> optional
失败是普通分支,需要错误信息
-> expected / Result<T, E>
结果是几种固定形态之一
-> variant
当前层无法处理,失败不属于常规控制流
-> exception例子:
text
find_user(id)
用户不存在是常见情况 -> optional<User>
parse_config(text)
失败要指出行号和原因 -> Result<Config, ParseError>
read_token()
token 可能是数字/标识符/字符串 -> variant
load_required_database()
程序无法继续启动 -> exception 可能合适7. 词汇类型是接口文档的一部分
对比:
cpp
std::string get_name();
std::string_view get_name_view();
std::optional<std::string> find_name();它们表达完全不同的语义:
text
std::string
返回拥有的字符串值
std::string_view
返回非拥有观察窗口,必须关心被观察对象生命周期
std::optional<std::string>
可能没有名字;有的话返回拥有的字符串类型本身就在提醒调用者该怎么用。
这就是词汇类型的价值。
8. 本篇总结
常见词汇类型:
text
optional<T>
可能有 T,也可能没有
variant<A, B, C>
有限几种类型之一
string_view
非拥有字符窗口
span<T>
非拥有连续对象窗口
expected<T, E> / Result<T, E>
成功有 T,失败有 E设计接口时问:
text
调用者是否需要知道可能没有值?
失败原因是否重要?
返回值是拥有对象,还是观察窗口?
状态是否是有限几种形态之一?
类型能否替我表达这些语义?现代 C++ 的一个方向,就是让接口少靠口头约定,多靠类型表达。