每周技巧 #123:`absl::optional` 和 `std::unique_ptr`
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #123: absl::optional and std::unique_ptr。
原文最初作为 totw/123 发布于 2016 年 9 月 6 日。
作者:Alexey Sokolov 和 Etienne Dechamps
快捷链接:abseil.io/tips/123
如何存储值
本技巧讨论几种存储值的方式。这里使用类成员变量作为示例,但下面许多要点也适用于局部变量。
|
|
作为裸对象
这是最简单的方式。val_ 分别在 Foo 构造函数开始处构造,并在 Foo 析构函数结束处销毁。如果 Bar 有默认构造函数,甚至不需要显式初始化它。
val_ 使用起来非常安全,因为它的值不可能为 null。这能消除一类潜在 bug。
但是裸对象不太灵活:
val_的生命周期从根本上绑定到其父对象Foo的生命周期,而这有时并不理想。如果Bar支持移动或交换操作,可以用这些操作替换val_的内容;但任何已有的指针或引用仍会指向或引用同一个val_对象(作为容器),而不是其中存储的值。- 任何需要传给
Bar构造函数的实参,都必须在Foo构造函数的初始化列表中计算出来;如果涉及复杂表达式,这可能很困难。
作为 absl::optional<Bar>
这是裸对象的简单性和 std::unique_ptr 的灵活性之间的一个良好折中。对象存储在 Foo 内部,但与裸对象不同,absl::optional 可以为空。可以在任意时刻通过赋值(opt_ = ...)填充它,也可以就地构造对象(opt_.emplace(...))。
由于对象以内联方式存储,和平铺的裸对象一样,关于在栈上分配大型对象的常见注意事项同样适用。还要注意,空的 absl::optional 和有值的 absl::optional 占用一样多的内存。
与裸对象相比,absl::optional 有几个缺点:
- 读者不太容易看出对象构造和析构发生在哪里。
- 存在访问一个并不存在的对象的风险。
作为 std::unique_ptr<Bar>
这是最灵活的方式。对象存储在 Foo 外部。和 absl::optional 一样,std::unique_ptr 可以为空。不过,与 absl::optional 不同,它可以把对象所有权转移给别的东西(通过移动操作),也可以从别的东西取得对象所有权(在构造或赋值时),还可以接管裸指针所有权(在构造时,或者通过 ptr_ = absl::WrapUnique(...);见 TotW 126)。
当 std::unique_ptr 为 null 时,它不会分配对象,只消耗一个指针大小的空间1。
如果对象可能需要活得比 std::unique_ptr 的作用域更久(所有权转移),就必须用 std::unique_ptr 包装它。
这种灵活性有一些成本:
- 读者的认知负担增加:
- 不太明显里面存的是什么(
Bar,还是派生自Bar的东西)。不过,这也可能降低认知负担,因为读者可以只关注指针持有的基类接口。 - 和
absl::optional相比,更不明显对象构造和析构发生在哪里,因为对象所有权可能被转移。
- 不太明显里面存的是什么(
- 和
absl::optional一样,存在访问一个并不存在的对象的风险,也就是著名的空指针解引用。 - 指针引入了额外一层间接访问,需要堆分配,并且对 CPU 缓存不友好;这是否重要,很大程度上取决于具体使用场景。
- 即使
Bar可复制,std::unique_ptr<Bar>也不可复制。这也会阻止Foo可复制。
结论
一如既往,应尽量避免不必要的复杂性,并使用能工作的最简单方案。如果裸对象适合你的场景,就优先使用裸对象。否则,尝试 absl::optional。最后不得已时,再使用 std::unique_ptr。
-
如果有非空的自定义删除器,还会为该删除器额外占用空间。 ↩︎