Exposing Containers of Unique Pointers

The std::unique_ptr is probably one of my favorite and most used features in modern C++. The unique pointer is as powerful as it is simple, and assuming you've used it before, you surely know how awesome it is.

There are many great articles written about unique pointers, so I won't go into detail. What I will mention though is that unique pointers provide ownership semantics. What this means is that the unique pointer follows a well defined set of rules regarding the lifetime of its allocated resource. As the name implies, std::unique_ptr is the single owner of the resource. It cannot be copied and when the unique pointer object is destructed, it takes care of deallocating the resource it holds.

Oftentimes, you own more than one instance of a certain object. Unique pointers work very well together with the STL, so it's not uncommon to see some class own a std::vector, or any other container, with unique pointers.

Let's take the class Bar as an example. It consists of a list of Foo's that are created elsewhere, for example by a factory responsible for initializing the appropriate subclass.

class Foo;  
class Bar {  
public:  
  Bar() = default;
  void addFoo(std::unique_ptr<Foo> foo) { foos.push_back(std::move(foo)); }
private:  
  std::vector<std::unique_ptr<Foo>> foos;
};

Now consider the situation where we want to expose the list of Foos to the user of the Bar class. What would our API look like? Usually we would solve this by adding a getter which returns a (const) reference to the vector.

const std::vector<std::unique_ptr<Foo>> &Bar::getFoos() const {  
  return foos; 
}

However, there's probably no reason to expose to the caller that Foo's are stored as unique pointers. Furthermore, in the non-const case, nothing prevents the caller from taking ownership of any Foo and possibly leave them in a moved-from state in the vector. If you really do want to give up ownership, it's better to return the container by value and call ::clear() on the container to prevent accidental user-after-moves.

A better approach would be to return a list of references to Foo. By returning references rather than pointers, you make it clear that the caller is not responsible for managing Foo's lifetime. As an added benefit, the resulting container can be manipulated by any algorithm in the Standard Template Library as if it contains the objects itself. This can lead to a lot cleaner code compared to having to account for the unique pointer everywhere.

std::vector<std::reference_wrapper<Foo>> Bar::getFoos() const {  
  std::vector<std::reference_wrapper<Foo>> fooRefs;
  for (auto &ptr : foos) {
    fooRefs.push_back(std::ref(*ptr));
  }
  return fooRefs;
}

Of course nothing is free and transforming the list incurs the cost of iterating over the whole container and doing a memory allocation for each item. This is also the biggest drawback and a trade-off you'll need to consider. If the function is called often and the original container changes rarely, you might want to cache the results.

Generalizing Our Approach

Rather than duplicating the logic for converting a container of unique pointers to references, we can make the code more generic so we can reuse it for any sequence container that takes a type and an allocator as a template argument. For std::vectors we can make the code a little more performant by pre-allocating space since we know the number of elements in advance.

template <template <typename, typename> class Container, typename Value,  
          typename Allocator = std::allocator<Value>>
auto to_refs(const Container<Value, Allocator> &container) {  
  Container<std::reference_wrapper<typename Value::element_type>, Allocator>
      refs;
  std::transform(container.begin(), container.end(), std::back_inserter(refs),
                 [](auto &x) { return std::ref(*x); });
  return refs;
}

template <typename Value,  
          typename Allocator = std::allocator<Value>>
auto to_refs(const ::std::vector<Value, Allocator> &container) {  
  std::vector<std::reference_wrapper<typename Value::element_type>, Allocator>
      refs;
  refs.reserve(container.size());
  std::transform(container.begin(), container.end(), std::back_inserter(refs),
                 [](auto &x) { return std::ref(*x); });
  return refs;
}

While this is already better than a ad-hoc solution, creating a new container can be wasteful if it's not strictly needed. If all we need is iterators into the container, we can omit creating a new container completely.

Iterators

Looking back at our original example, assume that we want to expose the vector of Foos via an iterator.

std::vector<std::unique_ptr<Foo>>::iterator Bar::begin() {  
  return foos.begin();
}
std::vector<std::unique_ptr<Foo>>::iterator Bar::end() {  
  return foos.end();
}

Once again we run into our original problem; we're exposing our clients to the fact that the objects are stored as unique pointers. We can remedy this by wrapping the iterators so that the elements are dereferenced in the iterator.

template <class BaseIterator> class DereferenceIterator : public BaseIterator {  
public:  
  using value_type = typename BaseIterator::value_type::element_type;
  using pointer = value_type *;
  using reference = value_type &;

  DereferenceIterator(const BaseIterator &other) : BaseIterator(other) {}

  reference operator*() const { return *(this->BaseIterator::operator*()); }
  pointer operator->() const { return this->BaseIterator::operator*().get(); }
  reference operator[](size_t n) const {
    return *(this->BaseIterator::operator[](n));
  }
};

template <typename Iterator> DereferenceIterator<Iterator> dereference_iterator(Iterator t) {  
  return DereferenceIterator<Iterator>(t);
}

We can change the return type of ::begin() and ::end() to DereferenceIterators to make them behave as if they were iterators into a container of Foos, completely transparent to the fact that they are actually stored as pointers.

DereferenceIterator<std::vector<std::unique_ptr<Foo>>::iterator> Bar::begin() {  
  return dereference_iterator(foos.begin());
}
DereferenceIterator<std::vector<std::unique_ptr<Foo>>::iterator> Bar::end() {  
  return dereference_iterator(foos.end());
}

Unfortunately the unique pointer type still shows up in the type of the iterator. With a typedef we could hide this fact too, as to not confuse the client.

Basically, what we have implemented now is Boost's indirect_iterator but for std::unique_ptrs. Although this might all seem a little convoluted at first sight, it has proven to be extremely useful in practice.

Jonas Devlieghere

Software Engineer at GuardSquare, working with LLVM on mobile application protection for iOS.

Subscribe to Jonas Devlieghere

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!