Tip of the Week #55: 名称计数和unique_ptr 原文链接
最初在2013-09-12发布为totw/55
作者:Titus Winters (titus@google.com)
更新:2017-10-20
地址:abseil.io/tips/55
虽然我们知道神灵有上千个名字,但他对我们大家都是一样的。 ——圣雄甘地。
通俗的说,变量的“名称(name)”是指在任何作用域、持有一个特定数据值的任意值类型的变量(而非指针或引用)。(语言律师听着,当我们说“名称”时实际上指的是左值。)由于std::unique_ptr的特定行为需求,我们需要保证std::unique_ptr包含的任意值只能有一个名称。
值得注意的是C++语言委员会赋予std::unique_ptr了一个非常恰当的名称。std::unique_ptr中存储的任意非空指针在任何时候只能在一个std::unique_ptr中出现,标准库的设计强制要求这点。使用std::unique_ptr的很多常见编译问题可以通过学习如何进行名称计数来解决:一个名称是可以的,但同一指针有多个名称就有问题了。
我们来数一下名称的个数。在每一行,数一下包含同一指针的std::unique_ptr的活着的名称的个数(无论是否在作用域中)。如果任意一行同一指针的名称个数超过1,那就是一个错误!
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); // DOES NOT COMPILE!
}
void SmarterThanTheCompilerButNot() {
Foo* j = new Foo(2);
// Compiles, BUT VIOLATES THE RULE and will double-delete at runtime.
std::unique_ptr<Foo> k(j);
std::unique_ptr<Foo> l(j);
}在Simple()中,NewFoo()分配的unique指针只有一个名称:AcceptFoo()中的"f"。
与上面相反,DoesNotBuild()中NewFoo()分配的unique指针有两个名称:DoesNotBuild()中的"g"和AcceptFoo()中的"f"。
这是经典的唯一性背离:执行中的任意给定点,std::unique_ptr持有的任意值(更一般的说,任意move-only类型)只能通过一个独特的名称来引用。任何会引入新名称的类似拷贝的行为都是禁止的,而且编译不通过。
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()。
void EraseTheName() {
std::unique_ptr<Foo> h = NewFoo();
AcceptFoo(std::move(h)); // Fixes DoesNotBuild with std::move
}std::move()就是一个有效的名称消除器,概念上来说你可以停止使用"h"作为指针的名称。现在通过了唯一名称规则:NewFoo()分配的unique指针有一个唯一的名称("h"),而在AcceptFoo()中也只有一个唯一的名称("f")。通过使用std::move(),我们承诺除非重新赋值给"h"不会再读它。
在现代c++中,名称计数对于那些不太擅长左值、右值等细节的人来说是一个方便的技巧:它可以帮助你识别到不必要拷贝的可能性,也能帮你正确的使用std::unique_ptr。计数之后如果某个点有太多的名称,请使用std::move来消除不需要的名称。