每周技巧 #123:`absl::optional` 和 `std::unique_ptr`

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #123: absl::optional and std::unique_ptr

原文最初作为 totw/123 发布于 2016 年 9 月 6 日。

作者:Alexey SokolovEtienne Dechamps

快捷链接:abseil.io/tips/123

如何存储值

本技巧讨论几种存储值的方式。这里使用类成员变量作为示例,但下面许多要点也适用于局部变量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <memory>
#include "absl/types/optional.h"
#include ".../bar.h"

class Foo {
  ...
 private:
  Bar val_;
  absl::optional<Bar> opt_;
  std::unique_ptr<Bar> ptr_;
};

作为裸对象

这是最简单的方式。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

每周技巧 #122:测试夹具、清晰性和数据流

上一节

每周技巧 #124:`absl::StrFormat()`

下一节

  1. 如果有非空的自定义删除器,还会为该删除器额外占用空间。 ↩︎