Tip of the Week #187: std::unique_ptr Must Be Moved

Originally 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.

What is a 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.

The Costs of 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.

Common Anti-Pattern: Avoiding &

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();
}

Common Anti-Pattern: Delayed Initialization

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_;
};

Caveats

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.

Large, rarely used objects.

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.

Legacy APIs

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.
  1. 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. 


Subscribe to the Abseil Blog