C++17 最值得先学的,不是把旧代码改写得更短,而是把几个不同的问题表达清楚:一个函数可能没有结果;一个事件可能有不同形态;一段文本可能只是暂时借来读取。类型选对了,调用方就更容易看见需要处理的情况。
本文只处理内存中的模拟读数,不连接传感器、不发送控制命令。上一节见 Strategy:分离稳定流程与可变行为,系列入口见 程序设计知识分类。
1. 先分清三个问题
optional<T>:有没有一个T?适合查询未命中、可选配置等“缺失本身已经足够说明问题”的接口。variant<A, B>:当前是哪一种状态?适合种类有限、各自携带不同字段的事件。string_view:这段字符在哪里、长度多少?它描述字符范围,不负责持有字符。
三者解决的问题不同。optional<string_view> 只增加“可能缺失”的状态,不会把借用变成所有权。variant 内的视图、指针或含借用成员的对象,也不会自动延长所指对象的生命期。
下面的模拟数据把负数读数视为不可用。查询返回的是整数副本,所以离开查询函数后仍然有效;无效读数和不存在的编号统一映射为“缺失”。如果调用方需要区分失败原因,就该改变接口,而不是约定更多魔法数字。
2. 完整程序:先查询,再生成事件
以下是全部源代码,可直接下载 cpp17_values.cpp。
|
在源码所在目录运行:
g++ -std=c++17 -Wall -Wextra -Werror -pedantic cpp17_values.cpp -o cpp17_values |
准确输出:
sensor=front |
已在 GCC 14.2.0、libstdc++ 14(__GLIBCXX__=20250315)下编译运行。这里不需要 C++20 才标准化的 <version>;用到的 C++17 库类型分别来自 <optional>、<variant>、<string_view>。切换其他编译器时,应同时核对语言实现和标准库实现,不能只看 -std=c++17 是否被接受。libstdc++ 实现状态
3. 结构化绑定:给组成部分命名
const auto& [id, millimeters] 是 C++17 的结构化绑定。面对 pair、元组、数组或满足规则的类对象,它把成员关系表达为名字。这里的名字对应原有 pair 的成员,避免复制整项数据。
注意声明中的引用:auto [a, b] = pair 会先形成一个隐藏的副本,再绑定其组成部分;auto& [a, b] = pair 则以原对象为基础。并不是只要出现方括号,就会自动按引用操作。引用形式也必须服从原对象的生命期和迭代器失效规则。C++17 工作草案 N4659:dcl.struct.bind
名字有助于表达领域含义,但不能补充缺失的领域约束。示例仍然需要显式判断编号与读数;结构化绑定不会验证单位、数值范围或重复编号。
4. optional:让“没有”进入返回类型
find_distance 返回 optional<int>,成功时装着一个整数,失败时返回 nullopt。这样 0 仍然可以是合法读数,不再被拿来暗示查询失败。
if (auto distance = ...; distance) 同时用了 C++17 的 if 初始化语句:查询结果只在这一整条 if 的作用域中存在。条件先确认有值,再使用 *distance。不要对空的 optional 直接解引用;value() 会在无值时抛出 bad_optional_access,但普通解引用没有这个检查保证。
value_or(0) 适合“业务确实接受零作为默认值”的情况,不能为了省去分支而掩盖缺失。这里保留分支,是因为输出层要把缺失展示出来。需要错误码、失败位置或重试原因时,可继续阅读 C++23 篇的 expected。
5. variant 与 if constexpr:让状态决定可用字段
Event 的候选类型在编译期列明:有效读数有 millimeters,缺失事件有 reason。相比一个结构体里同时放数值、原因和多枚布尔标志,这个模型减少了“标志说有效、字段却没有意义”的组合。
visit 根据当前候选类型调用访问器。泛型 lambda 的 item 类型随候选类型变化,decay_t 去掉这里的引用和 const 限定,if constexpr 再选择对应的代码。模板实例化时,条件确定后未选中的分支不会被实例化,因此 Reading 不必提供 reason 字段。
这不是文本预处理:代码仍须符合语法,不依赖模板参数的错误也不能靠它任意隐藏。模板之外的 if constexpr(false) 更不是跳过类型检查的通用方法。N4659:stmt.if、variant.visit
variant 适合封闭的类型集合;新增状态往往需要一起检查访问器。它也不是“永远恰有一个有效值”的绝对保证:某些类型切换操作遇到异常时可能进入 valueless_by_exception 状态,此时 visit 会抛出 bad_variant_access。本例只直接构造事件,不演示可能触发该状态的重新赋值。
6. string_view:省一次复制,也多一份生命期责任
full_name 是持有字符的 string,name 与其 substr 结果都是借用视图。调用 print_sensor 时,原字符串仍在 main 中存活且没有被修改,所以读取安全。string_view::substr 产生另一个视图,不复制字符;这与 string::substr 返回新字符串不同。
以下三类边界必须主动处理:
- 不能让视图逃出字符拥有者的生命期。例如把临时
string赋给一个长期保存的string_view,临时对象在语句结束后被销毁,视图就悬空了。 - 底层
string扩容、重分配或销毁后,原视图可能失效。视图不会更新自己,也不会保住旧内存。 - 视图的末尾不保证存在空字符。把
data()交给要求以\0结尾的旧接口,可能读取超出视图范围的内容。
只在函数调用期间读取文本时,string_view 很方便;需要保存到任务队列、对象成员或异步回调时,通常应保存拥有数据的 string。一次复制往往比一条隐含的生命期约定更便宜。
7. 把新语法放回设计中
本文的新设施属于 C++17,但“谁拥有数据、谁负责清理”的原则更早就存在。RAII 不是 C++17 新发明;unique_ptr 是 C++11,make_unique 是 C++14。新标准的价值在于帮助把这些原则写得更直接,而不是推翻旧的责任边界。
本文采用公开工作草案核对语义,具体运行结果来自上面注明的工具链。继续阅读 C++20 的约束、范围与视图,把单个值的接口扩展到批量数据;也可以回到 程序设计知识分类 查看完整主线。