Appearance
std::optional(C++17)
面试高频新特性组件。表示「可能存在的值」:要么含有值,要么为空(无值)。学会讲清「为什么需要、底层怎么存、怎么用、和指针/其它类型的区别」。
一、为什么需要 optional?
此前表达「可能没有结果 / 无效值」的办法都有缺点:
- 用指针判空:不安全、易悬垂、所有权语义混乱;
- 返回
-1/哨兵值:依赖约定、容易漏判、对非整数类型不优雅; - 抛异常:对「正常无值」情况过重。
optional<T> 语义清晰:「这个 int 可能没有」,读取前 has_value() / 安全访问即可。
cpp
#include <optional>
std::optional<int> find(const std::string& s, char c); // 找不到返回 nullopt
auto r = find("abc", 'x');
if (r) { // 或 r.has_value()
int v = r.value();
} else {
// 无值分支
}二、无值构造与安全的读取
cpp
std::optional<int> a; // 默认无值
std::optional<int> b = std::nullopt; // 显式无值
std::optional<int> c = 42; // 有值访问 API:
| 方法 | 无值时 |
|---|---|
operator* / operator-> | UB(必须先判空) |
value() | 抛 std::bad_optional_access |
value_or(fallback) | 返回 fallback |
has_value() / operator bool | 判空用 |
emplace(a...) / reset() | 构造/清空 |
三、底层实现(面试常问「optional 是怎么实现的」)
不堆分配 —— T 直接内联存储于 optional 内部。核心思想同手写容器:原始存储 + 按需构造 + 标记是否有值。
cpp
template <typename T>
class optional {
union {
// 用 union/字节数组预留存放 T 的空间,仅在需要时 placement new
};
bool has_value_;
// 内存布局 ≈ alignas(合适的存储) + bool
};- 它用 placement new 就地构造,析构时若
has_value才调~T()。 - 空 optional 大小通常 =
sizeof(T)(或 T+1 以放标志,受实现/对齐影响),不额外堆分配,比unique_ptr存动态对象更省。
因此手写 optional 的关键实现点:
- 用原始字节/对齐 buffer 保存潜在 T,不立刻构造默认 T。
- 构造/复制/移动/析构都判断 has_value 后再操作底层 T。
- 提供 value_or/value 等异常安全的读取。
四、高频问答
Q1. optional<T&> 支持吗?
C++17 的 optional 是 optional<T>,不支持引用类型 T(引用无法存进 union/需要 rebind 语义)。若想存「可空的引用」,用 optional<std::reference_wrapper<T>> 或指针。
Q2. optional vs unique_ptr/shared_ptr?
- shared/unique 表达「所有权 + 可能为空 + 动态对象」,需堆分配。
- optional 表达「值本身可有可无」且对象就地存放,通常更快(无堆分配)。
- 语义不同:optional 不是「指向对象的指针」,是「一个可能没值的值」。
Q3. optional 什么时候不该用?
- 若「空」代表严重错误并需要调用方能识别原因 → 用异常/错误码,不吞成普通空值。
- 大量频繁判断空值导致代码啰嗦时,也许更适合「值对象 + 哨兵」或直接返回可空指针(视场景)。
Q4. 与 std::variant/std::any 区别
- optional:T 有/无;
- variant:同时可能为 T1/T2/…(sum type);
- any:运行期任意类型(类型擦除)。 见
stl/string-and-optional.md(已整理对比表)。
五、手写思路 15 秒版
字节 buffer + has_value 标志;构造:new (&buf_) T(args...) 置 has;析构有值才 ~T();value() 判 has 否则抛;operator* 不加检查(同 std 语义)。