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 Declarationsstd::unique_ptr
Must Be MovedOriginally posted as TotW #187 on November 5, 2020
By Andy Soffer
Updated 2020-11-05
Quicklink: abseil.io/tips/187
If you say in the first chapter that there is a std::unique_ptr
on the wall,
in the second or third chapter it absolutely must be moved. If it’s not going to
be moved, it shouldn’t be hanging there. ~ With apologies to Anton Chekhov
std::unique_ptr
is for expressing transfer of ownership. If you never pass
ownership from one std::unique_ptr
to another, the abstraction is rarely
necessary or appropriate.
std::unique_ptr
?A std::unique_ptr
is a pointer that automatically destroys whatever it is
pointing at when the std::unique_ptr
itself is destroyed. It exists to convey
ownership (the responsibility to destroy resources) as part of the type system
and is one of C++11’s more valuable additions1. However,
std::unique_ptr
is commonly overused. A good litmus test is this: If it is
never std::move
d to or from another std::unique_ptr
, it likely should not be
a std::unique_ptr
. If we do not transfer ownership then there is almost
always a better way to express our intent than by using std::unique_ptr
.
std::unique_ptr
There are several reasons for avoiding std::unique_ptr
when ownership is not
being transferred.
std::unique_ptr
conveys transferrable ownership which is unhelpful if
ownership isn’t being transferred. We should aim to use the type that most
accurately conveys the required semantics.std::unique_ptr
can be in a null state, which gives extra cognitive
overhead for readers if the null state is not actually used.std::unique_ptr<T>
manages a heap-allocated T
, which comes with
performance implications both due to the heap allocation itself, and the
fact that the data is spread out across the heap and less likely to be in
CPU cache.&
It is not uncommon to see examples like the following.
int ComputeValue() { auto data = std::make_unique<Data>(); ModifiesData(data.get()); return data->GetValue(); }
In this example data
does not need to be a std::unique_ptr
, because
ownership is never transferred. The data will be constructed and destroyed
exactly at the same instances as if a Data
object were declared on the stack.
Therefore, as is also discussed in Tip #123, a better option would
be:
int ComputeValue() { Data data; ModifiesData(&data); return data.GetValue(); }
Because std::unique_ptr
is null when default constructed, and can be assigned
a new value from std::make_unique
, it’s common to see std::unique_ptr
used
as a delayed initialization mechanism. There is a particularly common pattern
with GoogleTest, in which test fixtures can initialize objects in SetUp
.
class MyTest : public testing::Test { public: void SetUp() override { thing_ = std::make_unique<Thing>(data_); } protected: Data data_; // Initialized in `SetUp()`, so we're using `std::unique_ptr` as a // delayed-initialization mechanism. std::unique_ptr<Thing> thing_; };
Once again, we see that ownership of thing_
is never transferred elsewhere, so
there is no need to use std::unique_ptr
. The example above could have done all
of the initialization in the default constructor for MyTest
. See the
GoogleTest FAQ
for details on SetUp
versus construction.
class MyTest : public testing::Test { public: MyTest() : thing_(data_) {} private: Data data_; Thing thing_; };
In this example, data_
is default constructed as it was before. Afterwards,
Thing
is constructed with data_
. Remember that a class’s constructor
initializes fields in the order they are declared, so this approach initializes
objects in the same order as they were before, but without the use of
std::unique_ptr
.
If delayed initialization is really important and unavoidable, consider using
std::optional
with its emplace()
method. Tip #123 discusses
delayed initialization in much greater depth.
class MyTest : public testing::Test { public: MyTest() { Initialize(&data_); thing_.emplace(data_); } private: Data data_; std::optional<Thing> thing_; };
This being C++, there are of course cases where a std::unique_ptr
makes sense
even if it is never moved. However these situations are uncommon, and any code
handling such situations should come with comments explaining the subtleties.
Here are two such examples.
If an object is only sometimes needed, std::optional
is a good default choice.
However, std::optional
reserves space regardless of whether the object is
actually constructed. If this space is important, it may make sense to hold a
std::unique_ptr
and only allocate it if it is needed.
Many legacy APIs return raw pointers to owned data. These APIs often predate the
addition of std::unique_ptr
to the C++ standard library, and this pattern
should not be copied in new code. However, even if the resulting object is never
moved, such legacy API calls should be wrapped in a std::unique_ptr
to ensure
that the memory is not leaked.
Widget *CreateLegacyWidget() { return new Widget; } int func() { Widget *w = CreateLegacyWidget(); return w->num_gadgets(); } // Memory leak!
Wrapping the object in a std::unique_ptr
solves both of these issues:
int func() { std::unique_ptr<Widget> w = absl::WrapUnique(CreateLegacyWidget()); return w->num_gadgets(); } // `w` is properly destroyed.
The word “unique” in the name std::unique_ptr
was chosen to signify
the idea that no other std::unique_ptr
should be holding the same
non-null value. That is, at any moment during program execution,
amongst all the std::unique_ptr
s that are not null, the addresses
held by all the std::unique_ptr
s are unique. ↩