string_viewoperator+ vs. StrCat()absl::Statusstd::bindabsl::optional and std::unique_ptrabsl::StrFormat()make_unique and private Constructors.boolexplicit= delete)switch Statements Responsibly= deleteAbslHashValue 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::moved 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_ptrThere 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_ptrs that are not null, the addresses
held by all the std::unique_ptrs are unique. ↩