Skip to content

手写 optional:union 原地版(精简 / 完整)与指针版 ​

optional 是「值语义 + 生命周期」的最小完整载体。常见形态为两问:先只读代码写出 ctor/dtor 时序, 再手写实现使输出逐行吻合;追问集中在存储位置、sizeof 与 placement new。 原理与标准库实现见 stl/optional.md。

一、题面结构 ​

阶段内容
第一问(只读,不运行)写出 value_or / has_value 的结果,以及 ctor/dtor 的完整打印时序
第二问(编码)手写 optional<T>,使程序产生第一问写出的输出

起始骨架通常只给三样:成员 T* obj; bool has_;、两个拷贝操作的声明、value_or 的半截实现; 移动构造/赋值显式 = delete。被测程序使用「每次操作都打印一行」的探针类来卡时序:

cpp
optional<Widget> x;      // 空
optional<Widget> y(20);  // 就位构造 -> Widget(20)
x.emplace();             // Widget default ctor
x = y;                   // 拷贝赋值(双方有值 -> 穿透赋值)
y.emplace(40);           // 先析构旧值,再构造新值
x.value().value(); y.value().value();   // 20 / 40

二、时序题答案 ​

text
Widget(20)
Widget default ctor
Widget copy assignment from 20 to 0
~Widget(20)
Widget(40)
x=20
y=40
~Widget(40)
~Widget(20)
text
has-value=0
value-or=<unset>
has-value=1
value-or=Hello QRT!
#输出行来源原因
1Widget(20)optional<Widget> y(20);就位构造:20 直接交给 Widget(int),无临时对象、无拷贝
2Widget default ctorx.emplace();x 原为空,emplace 不析构旧值,直接构造
3Widget copy assignment from 20 to 0x = y;双方有值 → 穿透赋值 *x = *y,而非析构 + 拷贝构造
4~Widget(20)y.emplace(40);emplace 先析构旧值
5Widget(40)同上再就位构造新值
6–7x=20 / y=40打印x 中是自 y 拷得的副本
8–9~Widget(40) / ~Widget(20)main 结束局部对象按声明逆序析构

易错点:将 has-value= 写成 true/false(std::cout << bool 默认输出 0/1); 将 x = y 视为「析构 + 拷贝构造」(输出会多两行);emplace 遗漏先析构旧值;析构顺序写成声明顺序。

三、接口与约束 ​

  • 接口:默认构造(空,不构造 T)、就位构造 U&&、拷贝构造、拷贝赋值、emplace(args...)、 reset、has_value、operator bool、value、operator*、operator->、value_or(U&&) const -> T
  • 不变量:has_ == true 时存储中恰有一个存活的 T;has_ == false 时没有
  • 被删除的操作:移动构造与移动赋值 = delete(题面约束),故清空只能靠 reset() / emplace()
  • 复杂度:emplace / 拷贝构造 / 析构为 O(1);指针版每次 engage 额外一次堆分配,原地版零分配
  • 异常安全:先 reset() 再构造,构造抛异常时保持空(基本保证)
  • 前置条件:仅当 has_value() 为真时可调用 value() / operator* / operator->(本实现不做检查)

四、精简版:union 原地、最小接口(先交这个) ​

实现范围:最小可用集 —— 默认构造、就位构造、拷贝构造/赋值、析构、emplace / reset / has_value / value(含 const 重载)。 未提供 operator bool / operator* / operator-> / value_or;移动操作按题面删除。

cpp
#pragma once
#include <new>
#include <type_traits>
#include <utility>

namespace qrt {
template <typename T>
class optional {
  union Storage {
    char dummy_;
    T value_;
    Storage() : dummy_() {}
    ~Storage() {}                       // 空析构:T 的生命周期由外层管理
  };

 public:
  optional() noexcept : has_(false) {}  // 空,不构造 T

  template <typename U, typename = std::enable_if_t<!std::is_same_v<std::decay_t<U>, optional> &&
                                                    std::is_constructible_v<T, U&&>>>
  optional(U&& value) : has_(true) { ::new (ptr()) T(std::forward<U>(value)); }

  optional(const optional& o) : has_(false) { if (o.has_) { ::new (ptr()) T(*o.ptr()); has_ = true; } }
  optional& operator=(const optional& o) {              // 双方有值 -> 穿透赋值
    if (this == &o) return *this;
    if (has_ && o.has_) s_.value_ = *o.ptr();
    else if (o.has_) { ::new (ptr()) T(*o.ptr()); has_ = true; }
    else reset();
    return *this;
  }
  optional(optional&&) = delete;
  optional& operator=(optional&&) = delete;
  ~optional() { reset(); }

  bool has_value() const { return has_; }
  T& value() { return s_.value_; }
  const T& value() const { return s_.value_; }

  template <typename... Args>
  T& emplace(Args&&... args) {                          // 先析构旧值,再就位构造
    if (has_) { has_ = false; s_.value_.~T(); }
    ::new (ptr()) T(std::forward<Args>(args)...);
    has_ = true;
    return s_.value_;
  }
  void reset() { if (has_) { has_ = false; s_.value_.~T(); } }

 private:
  T* ptr() { return std::addressof(s_.value_); }
  const T* ptr() const { return std::addressof(s_.value_); }
  Storage s_;
  bool has_;
};
}  // namespace qrt

小问答 ​

  • 这个版本差在哪? 只有 value() 一种访问方式,无 operator bool / operator* / operator-> / value_or。
  • 与 std::optional 的差别? 无 nullopt_t / in_place_t 构造、无比较运算符、无 and_then / transform / or_else;value() 不做检查(标准版抛 bad_optional_access)。
  • 为什么就位构造那行约束不能省? 无约束的 template <typename U> optional(U&&) 对 optional& 是恒等匹配,会劫持拷贝构造并尝试构造 T(optional<T>)。
  • 能直接用于时序题吗? 能。实测输出与题面期望的 9 行逐行一致(emplace 先析构旧值、拷贝赋值穿透、析构逆序)。

五、完整版:union 原地、完整接口 ​

实现范围:在精简版基础上补齐访问接口 —— operator bool、operator*(含 const)、operator->、 value_or;存储方式与精简版相同(零堆分配)。未实现 at / swap / 比较运算符。

cpp
namespace qrt {
template <typename T>
class optional {
  union Storage {
    char dummy_;
    T value_;
    Storage() : dummy_() {}   // 占位成员:使 union 非空且默认构造平凡
    ~Storage() {}             // 空析构:T 的生命周期由外层管理
  };

 public:
  optional() noexcept : has_(false) {}

  template <typename U, typename = std::enable_if_t<!std::is_same_v<std::decay_t<U>, optional> &&
                                                    std::is_constructible_v<T, U&&>>>
  optional(U&& value) : has_(true) { ::new (ptr()) T(std::forward<U>(value)); }

  optional(const optional& o) : has_(false) { if (o.has_) { ::new (ptr()) T(*o.ptr()); has_ = true; } }

  optional& operator=(const optional& o) {           // 双方有值 -> 穿透赋值
    if (this == &o) return *this;
    if (has_ && o.has_) s_.value_ = *o.ptr();
    else if (o.has_) { ::new (ptr()) T(*o.ptr()); has_ = true; }
    else reset();
    return *this;
  }

  optional(optional&&) = delete;
  optional& operator=(optional&&) = delete;
  ~optional() { reset(); }

  void reset() { if (has_) { has_ = false; s_.value_.~T(); } }
  template <typename... Args>
  T& emplace(Args&&... args) {
    if (has_) { has_ = false; s_.value_.~T(); }
    ::new (ptr()) T(std::forward<Args>(args)...);
    has_ = true;
    return s_.value_;
  }

  bool has_value() const { return has_; }
  explicit operator bool() const { return has_; }
  T& value() { return s_.value_; }
  const T& value() const { return s_.value_; }
  T& operator*() { return s_.value_; }
  const T& operator*() const { return s_.value_; }
  T* operator->() { return ptr(); }
  const T* operator->() const { return ptr(); }

  template <typename U>
  T value_or(U&& default_value) const { return has_ ? s_.value_ : static_cast<T>(std::forward<U>(default_value)); }

 private:
  T* ptr() { return std::addressof(s_.value_); }
  const T* ptr() const { return std::addressof(s_.value_); }

  Storage s_;
  bool has_;
};
}  // namespace qrt

技术选型 ​

  • 存储:union { char dummy_; T value_; } + bool has_
    • 四步流程:union 提供存储 → placement new 构造 → 手动调用析构 → bool 记录状态。
    • 对齐自动继承 T,避免 alignas + reinterpret_cast 带来的严格别名与 std::launder 问题。
  • 必须有平凡占位成员 dummy_,并自行定义 union 的构造与析构
    • 裸写 union { T value; } 不可行:T 非平凡时该 union 的默认构造与析构被定义为删除 (实测 NaiveUnion<std::string> 两者均不可用,NaiveUnion<int> 可用)。
    • union 析构保持为空,T 的生命周期由外层 reset() / emplace() 显式管理。
  • 取地址用 std::addressof
    • 对重载了 operator& 的类型仍能取得真实地址。
  • 原地化是纯存储替换
    • 两版实现接到同一 main() 上输出完全相同,ctor/dtor 时序不受存储方式影响。

小问答 ​

  • 比精简版多了什么? operator bool、operator*、operator->、value_or 及相应的 const 重载。
  • 与 STL 还差什么? 无 nullopt / in_place 构造、无比较与 swap、无 and_then / transform;value_or 未对可转换性做约束,value() 不抛异常。
  • 为什么必须手动析构? union 的析构是空的,T 的生命周期只能由 reset() / emplace() 显式管理 —— 这正是「四步」的后两步。
  • constexpr 能到哪一步? 需要 std::construct_at(C++20)替代 placement new,reinterpret_cast 在常量求值中被禁止。

六、配套:指针版(骨架给定 T* obj; bool has_; 时的等价实现) ​

实现范围:完整实现题面接口(默认构造、就位构造、拷贝构造/赋值、emplace / reset / has_value / operator bool / value / operator* / operator-> / value_or),移动操作按题面删除。 未实现 at、swap、比较运算符与 and_then 等扩展接口。

cpp
namespace qrt {
template <typename T>
class optional {
  T* obj = nullptr;
  bool has_ = false;

 public:
  optional() noexcept = default;                     // 不构造 T

  // 就位构造:约束必需,否则该模板对 optional& 为恒等匹配,会劫持拷贝构造
  template <typename U,
            typename = std::enable_if_t<!std::is_same_v<std::decay_t<U>, optional> &&
                                        std::is_constructible_v<T, U&&>>>
  optional(U&& value) : obj(new T(std::forward<U>(value))), has_(true) {}

  optional(const optional& o) : obj(o.has_ ? new T(*o.obj) : nullptr), has_(o.has_) {}

  optional& operator=(const optional& o) {           // 三路分支
    if (this == &o) return *this;                    // 自赋值
    if (has_ && o.has_) *obj = *o.obj;               // 双方有值 -> 穿透赋值
    else if (o.has_) { obj = new T(*o.obj); has_ = true; }   // 目标空 -> 拷贝构造
    else reset();                                    // 源空 -> 清空目标
    return *this;
  }

  optional(optional&&) = delete;
  optional& operator=(optional&&) = delete;

  ~optional() { reset(); }
  void reset() { if (has_) { has_ = false; delete obj; obj = nullptr; } }

  template <typename... Args>
  T& emplace(Args&&... args) {                       // 先析构旧值,再就位构造
    if (has_) reset();
    obj = new T(std::forward<Args>(args)...);
    has_ = true;
    return *obj;
  }

  bool has_value() const { return has_; }
  explicit operator bool() const { return has_; }
  T& value() { return *obj; }
  const T& value() const { return *obj; }
  T& operator*() { return *obj; }
  const T& operator*() const { return *obj; }
  T* operator->() { return obj; }
  const T* operator->() const { return obj; }

  template <typename U>
  T value_or(U&& default_value) const {              // 按值返回
    return has_ ? *obj : static_cast<T>(std::forward<U>(default_value));
  }
};
}  // namespace qrt

技术选型 ​

  • 按骨架使用堆上的 T* + bool 存储
    • 骨架已把存储写死为指针,实现只需满足语义;换来 sizeof 恒为 16(与 T 无关)。
    • 代价:每次 engage 一次堆分配、访问多一次间接寻址、对齐只继承指针的对齐。
  • 就位构造模板必须加 enable_if 约束
    • 否则该模板对 optional& 是恒等匹配,会抢走拷贝构造,进而尝试构造 T(optional<T>)。
    • 约束为「不是 optional 自身」且「T 可由该实参构造」。
  • 拷贝赋值分三路
    • 双方有值 → 穿透赋值给 T;目标空、源有值 → 拷贝构造;源空 → 清空目标。
    • 若统一写成「析构 + 拷贝构造」,可观测行为会变(多一次析构与一次拷贝构造),并多一次分配。
    • 单独判断 this == &o 以保证自赋值安全。
  • emplace 先析构旧值再构造
    • 与题面要求的时序一致;空对象上不执行析构。
  • value_or 按值返回
    • 返回引用在「空则返回默认值」的分支会悬垂。

小问答 ​

  • 与 union 版的差别? 每次 engage 一次堆分配、访问多一次间接寻址、sizeof 恒为 16、对齐只继承指针;换来的是实现更短、可照骨架直接写。
  • 为什么题面会给这种骨架? 存储被写死为 T*,考点落在「拷贝语义与 ctor/dtor 时序」,而不是存储布局。
  • 与 STL 的差别? 同完整版缺的部分;另外它手写拷贝,trivially_copyable 为 false。
  • 两版能互换吗? 接口一致,实测输出相同,可整类替换。

七、高频问答 ​

Q1. 标准库的 optional 存在哪? ​

原地存储:union { _Empty_byte _M_empty; _Stored_type _M_value; } 加 bool _M_engaged。 构造为 placement new,销毁为显式析构调用,无堆分配。

Q2. sizeof 如何比较? ​

Tsizeof(T)std::optional<T>指针版
int4816
char1216
double81616
std::string324016

小类型下原地版明显更省;T 较大时指针版「看起来」更小,但额外一次堆分配 (分配器头部约 8–16 字节与碎片)使总面积反而更大。指针版省的是编译期大小,付的是运行期的分配与间接寻址。

Q3. sizeof(bool) 是 1 字节还是 1 bit? ​

1 字节。标准规定 sizeof(bool) 为实现定义,但 bool 是对象,至少占 1 字节(标准脚注明确不要求为 1)。 按位存储的是 std::vector<bool> 特化、位域与 bitset。因此「指针 + bool」为 9 字节, 按 alignof(void*) = 8 向上取整为 16,尾部 7 字节为 padding。

Q4. 能否压缩到 8 字节? ​

只能改变语义:以 nullptr 表示空(optional<T*>),或利用指针低位的 tag(要求对象 2 字节对齐)。 packed 得到 9 字节且可能非对齐访问。x86-64 的指针没有空闲位可复用。

Q5. std::optional 移动后源对象的状态? ​

仍为 has_value() == true,只是 T 处于 moved-from 状态;清空需显式 reset()。

Q6. value_or 为什么按值返回? ​

接口约定。返回引用在「空则返回默认值」的分支会悬垂。

Q7. emplace 抛异常会留下半状态吗? ​

先 reset() 再构造,构造抛异常时保持空(基本保证);若需强保证,应先构造临时对象再替换。

Q8. 空 optional 上调用 value()? ​

本实现中是不加检查的 UB;std::optional::value() 抛 bad_optional_access,operator* 同样不检查。 取舍为「零开销」与「运行期检查」之间的选择。

Q9. 拷贝赋值为何必须三分支? ​

语义与可观测行为均不同:双方有值时为穿透赋值(一次 copy assignment), 若统一写成「析构 + 拷贝构造」则多一次析构与一次拷贝构造。

Q10. 原地版为何可 constexpr,指针版不可? ​

new / delete 不能用于常量求值;C++20 起 std::construct_at 配合 union 可以。

八、常见错误 ​

错误后果
拷贝赋值写成「析构 + 拷贝构造」输出多出 ~Widget(0) 与 Widget copy ctor ...
emplace 未先析构旧值,或先构造后析构时序输出与预期不符
默认构造中执行 new T()optional<T> x; 即打印构造
缺少析构,或 delete 后未将 has_ 置回 false无输出 / double free
就位构造模板缺少约束拷贝构造被劫持,编译失败或选错重载
value() 只提供非 const 版本const 上下文取不到
std::addressof 写成 &重载了 operator& 的类型出错

糌粑的面经笔记 · VitePress 版