每周技巧 #55:名称计数与 unique_ptr

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #55: Name Counting and unique_ptr

原文最初作为 totw/55 发布于 2013 年 9 月 12 日。

作者:Titus Winters

更新于 2017 年 10 月 20 日。

“虽然我们可以用一千个名字认识他,但对我们所有人来说,他始终是同一个。”– Mahatma Gandhi

通俗地说,一个值的“名字”是指任何作用域中持有某个特定数据值的值类型变量(不是指针,也不是引用)。(对规范律师来说,如果我们说“名字”,本质上是在谈 lvalue。)由于 std::unique_ptr 的特定行为要求,我们需要确保 std::unique_ptr 中持有的任何值都只有一个名字。

值得注意的是,C++ 语言委员会给 std::unique_ptr 选了一个非常贴切的名字。存储在 std::unique_ptr 中的任何非空指针值,在任意时刻都必须只出现在一个 std::unique_ptr 中;标准库就是按这个目标设计来强制执行的。很多使用 std::unique_ptr 的代码编译问题,都可以通过学会识别如何给 std::unique_ptr 计数名字来解决:一个名字没问题,但同一个指针值有多个名字就不行。

我们来数几个名字。在每一行编号处,数一数此时还活着的、引用同一个指针的 std::unique_ptr 名字有多少个(无论是否在作用域内)。如果你发现某一行里同一个指针值有超过一个名字,那就是错误!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
std::unique_ptr<Foo> NewFoo() {
  return std::unique_ptr<Foo>(new Foo(1));
}

void AcceptFoo(std::unique_ptr<Foo> f) { f->PrintDebugString(); }

void Simple() {
  AcceptFoo(NewFoo());
}

void DoesNotBuild() {
  std::unique_ptr<Foo> g = NewFoo();
  AcceptFoo(g); // 无法编译!
}

void SmarterThanTheCompilerButNot() {
  Foo* j = new Foo(2);
  // 可以编译,但违反规则,运行时会 double-delete。
  std::unique_ptr<Foo> k(j);
  std::unique_ptr<Foo> l(j);
}

Simple() 中,通过 NewFoo() 分配的 unique pointer 始终只有一个你可以用来引用它的名字:AcceptFoo() 内部的名字 “f”。

和它对比的是 DoesNotBuild():通过 NewFoo() 分配的 unique pointer 有两个名字引用它:DoesNotBuild() 中的 “g” 和 AcceptFoo() 中的 “f”。

这是典型的唯一性违规:在执行过程中的任意给定时刻,std::unique_ptr 持有的任何值(更一般地说,任何只可移动类型持有的值)只能被一个不同的名字引用。任何看起来像会引入额外名字的拷贝都是被禁止的,并且无法编译:

1
2
scratch.cc: error: call to deleted constructor of std::unique_ptr<Foo>'
  AcceptFoo(g);

即使编译器没有抓住你,std::unique_ptr 的运行时行为也会抓住你。任何时候,只要你“聪明过了编译器”(见 SmarterThanTheCompilerButNot())并引入多个 std::unique_ptr 名字,它也许能编译(暂时),但你会遇到运行时内存问题。

现在问题变成:我们如何移除一个名字?C++11 也提供了解法,就是 std::move()

1
2
3
4
void EraseTheName() {
  std::unique_ptr<Foo> h = NewFoo();
  AcceptFoo(std::move(h)); // 用 std::move 修复 DoesNotBuild
}

std::move() 的调用实际上是一个名字擦除器:从概念上讲,你可以不再把 “h” 计为这个指针值的名字。现在它通过了不同名字规则:通过 NewFoo() 分配的 unique pointer 有一个名字(“h”),而在调用 AcceptFoo() 期间也再次只有一个名字(“f”)。通过使用 std::move(),我们承诺在给 “h” 重新赋新值之前,不会再读取它。

名称计数是现代 C++ 里的一个实用技巧,尤其适合那些还不熟悉 lvalue、rvalue 等细节的人:它可以帮助你识别不必要拷贝的可能性,也会帮助你正确使用 std::unique_ptr。计数之后,如果你发现某个点名字太多,就用 std::move 擦掉那个不再需要的名字。

每周技巧 #49:实参依赖查找

上一节

每周技巧 #59:连接元组

下一节


本节目录