“尽管我们可能以一千种名字来认识他,但是他于我们所有人而言是同一个人。”——圣雄甘地

通俗来讲,值的“名字”是在任意作用域内保持特定数值的任意值类型变量(不是指针也不是引用)。(对于规范准则而言,如果我们说“名字”,实质上我们谈论的是左值。)因为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);
// 可以编译,但是违反规则并且在运行时会重复删除
std::unique_ptr<Foo> k(j);
std::unique_ptr<Foo> l(j);
}

Simple()中,随NewFoo()分配的唯一指针只有一个可以引用的名字:在AcceptFoo()中的“f”。

DoesNotBuild()相比,随NewFoo()分配的唯一指针有两个引用它的名字:DoesNotBuild()中的”g”和AcceptFoo()中的“f”。

这时一个典型的违反唯一性:在执行中的任何给定点,通过std::unique_ptr(更确切的说,任何仅能够移动的类型)拥有的任何值都只能由单一明显的名字来引用。任何看起来像引用的额外名字的拷贝,都是禁止的,并且不能编译。

1
2
scratch.cc: 错误: 调用std::unique_ptr<Foo>被废弃了的构造函数
AcceptFoo(g);

即便编译器没有抓到你的问题,std::unique_ptr的运行时行为也会发生。每当你“自已为超过”编译器,然后引入多个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”。现在,这通过distinct-names规则:在NewFoo()分配的唯一指针有单个的名字(”h”),并且在调用AcceptFoo()中,这里又仅有单个名字(”f”)。通过使用std::move,我们保证直到为它分配了新值,才会再次读取“h”。

在现代C++中,对于那些不熟悉左值、右值等细节的人而言,名字计数是一种方便的技巧:它能够帮助你识别不必要拷贝的可能性,并且它能够帮助你恰当的使用std::unique_ptr。名字计数以后,如果你在一个地方发现过多的名字,请使用std::move来删除不再需要的名字。