本周小贴士#123: absl::optional和std::unique_ptr

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周小贴士#123: absl::optional和std::unique_ptr相关的知识,希望对你有一定的参考价值。

作为totW#123最初发表于2016年9月6日

由Alexey Sokolov (sokolov@google.com) and Etienne Dechamps (edechamps@google.com)创作

如何存储值?

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

#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。这消除了一类潜在的错误。

但是bare对象不是很灵活:

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

作为absl::optinal

纯对象的简单性和std::unqiue_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作为最后的方法。

Barabsl::optionalstd::unique_ptr
支持延迟构造
一直安全访问
能够转移Bar的所有权
能够存储Bar的子类
可移动性如果Bar可移动如果Bar可移动
可复制性如果Bar可复制如果Bar可复制
CPU缓存友好
无堆分配开销
内存用法sizeof(Bar)sizeof(Bar) + sizeof(bool)为空时sizeof(Bar*),否则sizeof(Bar*) + sizeof(Bar)
对象生命周期和封闭作用域相同严格限定封闭作用域不严格
调用f(Bar*)f(&val)f(&opt_.value())或f(&opt_)f(ptr_.get()) 或f(&*ptr_)
移除值N/Aopt_.reset(); 或 opt_ = absl::nullopt;ptr_.reset(); 或 ptr_ = nullptr;
  1. 在非空自定义删除器的情况下,该删除器还有一个额外的空间。 ↩
  2. 也可以添加填充。

以上是关于本周小贴士#123: absl::optional和std::unique_ptr的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#107:引用生命周期的扩展

本周小贴士#59:连接元组

本周小贴士#143:C++11 删除的函数(= delete)

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: Using声明和命名空间别名

本周小贴士#90:退役标志