如何存储值?

此贴士讨论了集中存储值得方法。此处我们使用类成员变量作为示例,但是以下的许多同样也适用于局部变量。

1
2
3
4
5
6
7
8
9
10
11
#include <memory>
#include "third_party/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。这小出了一类潜在的错误。

但是bar对象不是很灵活:

  • val_的生命周期基本与它的父Foo对象的生命周期相关,这有时并不是想要的。如果Bar支持移动或交换操作,那么能够通过这些操作替换val_的内容,然而对val_的任何现有的指针或引用继续指向或引用相同的val_对象(作为容器),而不是存储在其中的值。
  • 需要传递给Bar的构造函数的任何参数都需要在Foo的构造函数的初始化列表中进行计算,如果涉及复杂的表达式,这可能是困难的。

作为absl::optional

纯对象的简单性和std::unique_ptr的灵活性。对象存储在Foo中,但与纯对象不同,absl::optional可以为空。它可以随时通过赋值(opt_=)或通过在适当位置构造对象来输入。

因为是内联存储的,所以在栈上分配大对象的常见警告同样适用。另外请注意,空absl::optional使用的内存和输入的内存一样多。

与纯对象相比,absl::optional有一些缺点:

  • 对于读者来说,对象的构造和析构的地方都不明显
  • 对访问不存在对象的风险

作为std::unique_ptr

这是最灵活的方法。对象存储在Foo外,就像absl::optional一样,std::unique_ptr能够为空。然而,不像absl::optional,它可以将对象的所有权转移给其他对象(通过移动操作),从其他对象获取对象的所有权(通过构造函数或者赋值函数),或者假定对某个对象的原生指针的所有权(在构造或通过ptr_=absl::WrapUnique(...)),参见TotW126

std::unique_ptr作为null时,它没有分配对象,只消耗指针1的大小。

如果对象可能需要超出std::unique_ptr的作用域(所有权转移),那么std::unique_ptr中包装对象时必须的。

这种灵活性伴随着一些成本:

  • 增加读者的认知负担:
    • 不容易知道里面存储了什么(Bar,或从Bar派生的)。然而,它同样也减少认知负担,因为读者只聚集于所持有的基本接口
    • 在对象构造或析构的地方,它甚至比absl::optional更不明显,因为对象的所有权可以转移
  • absl::optional一样,有访问不存在对象的风险——著名的空指针解引用
  • 指针引用了额外的间接层,那么需要进行对分配,并且对CPU缓存不友好;重要与否依赖于特定的用例
  • std::unique_ptr即便是不可复制的。这依然可以放置Foo可复制

结论

与往常一样,努力避免不必要的复杂性,并使用最简单的东西。如果适用你的情况,优先穿对象。否则尝试absl::optional,请适用std::unique_ptr作为最后的方法。