string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally published as totw/55 on 2013-09-12
by Titus Winters ([email protected])
Updated 2017-10-20
Quicklink: abseil.io/tips/55
“Though we may know Him by a thousand names, He is one and the same to us all.” - Mahatma Gandhi
Colloquially, a “name” for a value is any value-typed variable (not a pointer,
nor a reference), in any scope, that holds a particular data value. (For the
spec-lawyers, if we say “name” we’re essentially talking about lvalues.) Because
of std::unique_ptr
’s specific behavioral requirements, we need to make sure
that any value held in a std::unique_ptr
only has one name.
It’s important to note that the C++ language committee picked a very apt name
for std::unique_ptr
. Any non-null pointer value stored in a std::unique_ptr
must occur in only one std::unique_ptr
at any time; the standard library is
designed to enforce this. Many common problems compiling code that uses
std::unique_ptr
can be resolved by learning to recognize how to count the
names for a std::unique_ptr
: one is OK, but multiple names for the same
pointer value are not.
Let’s count some names. At each line number, count the number of names alive at
that point (whether in scope or not) that refer to a std::unique_ptr
containing the same pointer. If you find any line with more than one name for
the same pointer value, that’s an error!
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);
}
In Simple()
, the unique pointer allocated with NewFoo()
only ever has one
name by which you could refer it: the name “f” inside AcceptFoo()
.
Contrast this with DoesNotBuild()
: the unique pointer allocated with
NewFoo()
has two names which refer to it: DoesNotBuild()
’s “g” and
AcceptFoo()
’s “f”.
This is the classic uniqueness violation: at any given point in the execution,
any value held by a std::unique_ptr
(or more generally, any move-only type)
can only be referred to by a single distinct name. Anything that looks like a
copy introducing an additional name is forbidden and won’t compile:
scratch.cc: error: call to deleted constructor of std::unique_ptr<Foo>'
AcceptFoo(g);
Even if the compiler doesn’t catch you, the runtime behavior of
std::unique_ptr
will. Any time where you “outsmart” the compiler (see
SmarterThanTheCompilerButNot()
) and introduce multiple std::unique_ptr
names, it may compile (for now) but you’ll get a run-time memory problem.
Now the question becomes: how do we remove a name? C++11 provides a solution for
that as well, in the form of std::move()
.
void EraseTheName() {
std::unique_ptr<Foo> h = NewFoo();
AcceptFoo(std::move(h)); // Fixes DoesNotBuild with std::move
}
The call to std::move()
is effectively a name-eraser: conceptually you can
stop counting “h” as a name for the pointer value. This now passes the
distinct-names rule: on the unique pointer allocated with NewFoo()
has a
single name (“h”), and within the call to AcceptFoo()
there is again only a
single name (“f”). By using std::move()
we promise that we will not read
from “h” again until we assign a new value to it.
Name counting is a handy trick in modern C++ for those that aren’t expert in the
subtleties of lvalues, rvalues, etc: it can help you recognize the possibility
of unnecessary copies, and it will help you use std::unique_ptr
properly.
After counting, if you discover a point where there are too many names, use
std::move
to erase the no-longer-necessary name.