Appearance
手写 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!| # | 输出行 | 来源 | 原因 |
|---|---|---|---|
| 1 | Widget(20) | optional<Widget> y(20); | 就位构造:20 直接交给 Widget(int),无临时对象、无拷贝 |
| 2 | Widget default ctor | x.emplace(); | x 原为空,emplace 不析构旧值,直接构造 |
| 3 | Widget copy assignment from 20 to 0 | x = y; | 双方有值 → 穿透赋值 *x = *y,而非析构 + 拷贝构造 |
| 4 | ~Widget(20) | y.emplace(40); | emplace 先析构旧值 |
| 5 | Widget(40) | 同上 | 再就位构造新值 |
| 6–7 | x=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提供存储 → placementnew构造 → 手动调用析构 →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 如何比较?
T | sizeof(T) | std::optional<T> | 指针版 |
|---|---|---|---|
int | 4 | 8 | 16 |
char | 1 | 2 | 16 |
double | 8 | 16 | 16 |
std::string | 32 | 40 | 16 |
小类型下原地版明显更省;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& 的类型出错 |