Resource Management in C++

Dated May 31, 2022; last modified on Tue, 31 May 2022

Resource Handles

The constructor/destructor pattern enables objects defined in a scope to release the resources during exit from the scope, even when exceptions are thrown. All standard-library containers, e.g. std::vector, are implemented as resource handles.

std::unique_ptr and std::shared_ptr

These “smart pointers” are useful in managing objects that are allocated on the free store (as opposed to those allocated on the stack).

A unique_ptr ensures that an object is properly destroyed when the unique_ptr goes out of scope, e.g.

void f(int i, int j) {
  X* p = new X;
  std::unique_ptr<X> sp {new X};

  if (i < 99) throw Z{};  // p does not get deleted when we exit f
  if (j < 77) return;     // p does not get deleted when we exit f

  // ... use p and sp ...
  delete p;
} // sp will delete the managed X

// Admittedly, the following scheme is cleaner.
void f(int i, int j) {
  X x; // Allocated on the stack. Will be cleaned up on exiting f.
  // ...
}

When pointer semantics are needed, e.g. passing free-store allocated objects in and out of functions, a unique_ptr has no space or time overhead compared to correct use of a built-in pointer:

std::unique_ptr<X> make_X(int i) {
  // ... check i, etc. ...
  return std::unique_ptr<X>{new X{i}};
}

Why not use a std::optional?

A shared_ptr is used to model shared ownership. The object is deleted when the last of its shared_ptrs is destroyed, e.g.

void f(std::shared_ptr<fstream>);
void g(std::shared_ptr<fstream>);

void user(const std::string& name, base::openmode mode) {
  std::shared_ptr<fstream> fp {new fstream(name, mode)};
  if (!*fp) throw No_file{};

  f(fp);
  g(fp);
}

Although shared_ptrs are neither cost-free nor exorbitantly expensive, they make the lifetime of the shared object hard to predict, e.g. f() spawns a task that holds a copy of fp and outlives user.

To reduce verbosity, the use of new, and possible bugs (e.g. passing a pointer of something that’s not on the free-store), prefer to use std::make_unique and std::make_shared, e.g.

struct S { int i; std::string s; double d; };

auto p1 = std::make_unique<S>(1, "Ankh Morpork", 4.65);

// Furthermore, `make_shared` is more efficient because it does not need
// a separate allocation for the use count.
auto p2 = std::make_shared<S>(2, "Oz", 7.62);

Prefer using resource handles, and only use “smart pointers” when pointer semantics are needed:

  • When we share an object, we need pointers (or references) to refer to the shared object.
  • When we refer to a polymorphic object , we need a pointer (or a reference) because we don’t know the exact type of the object referred to.
  • When using legacy APIs that return raw pointers to owned data.

In particular, a pointer is not need to return a collection of objects from a function. A resource handle suffices.

When ownership is not transferred, a std::unique_ptr is likely not needed. Furthermore, we’d have cognitive overhead for “what if the pointer is empty?”, and performance implications for the heap-allocated object (less likely to be in CPU cache).

std::move and std::forward

A unique_ptr is the sole owner of an object, and therefore it cannot be copied. It must be explicitly moved if needed elsewhere.

Think of std::move as rvalue_cast. It does not move anything. Instead, it casts its argument to an rvalue reference, thereby saying that the argument will not be used again and therefore may be moved, e.g.

template <typename T>
void swap(T& a, T& b) {
  T tmp {std::move(a)};   // The T constructor sees an rvalue and moves
  a = std::move(b);       // The T assignment sees an rvalue and moves
  b = std::move(tmp);     // The T assignment sees an rvalue and moves
}

Unless you can demonstrate significant and necessary perf improvements, avoid uses of std::move that leave behind a moved-from object that may get used again. The state of a moved-from object is generally unspecified, but STL moved-from objects are in a state where they can be destroyed and assigned to, and furthermore, STL containers are in an “empty” state.

std::forward is useful when transmitting a set of arguments on to another function without changing anything. Once you forward an object, don’t use it again (including forwarding it a second time). std::forward is also a bit more sophisticated than std::move when handling lvalue and rvalue subtleties.

template <typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
  return unique_ptr<T>{new T{std::forward<Args>(args)...}};
}

References

  1. A Tour of C++ (Second Edition). Chapter 13. Utilities. Bjarne Stroustrup. 2018. ISBN: 978-0-13-499783-4 .
  2. abseil / Tip of the Week #187: std::unique_ptr Must Be Moved. abseil.io . Accessed May 31, 2022.