Mix boost::optional and std::unique_ptr

Viewed 2912

I admit it: I'm in love with the concept of optional. The quality of my code has improved so much ever since I discovered it. Making it explicit whether a variable may or may not be valid is so much better than plain error codes and in-band signaling. It also allows me to not worry about having to read the contract in the documentation, or worrying about whether it's up-to-date: the code itself is the contract.

That said, sometimes I need to deal with std::unique_ptr. Objects of this type might be null or not; at a given point in the code is impossible to know whether the std::unique_ptr is supposed to have a value or not; it's impossible to know the contract from the code.

I would like to somehow mix optional (maybe withboost::optional) and std::unique_ptr, so that I have a dynamically allocated object with scope-destruction and proper copy/move behaviour that explicitly states that it may not have a value. That way, I can use this new type to make it explicit that a check for value is necessary and avoid unnecessary checks for plain std::unique_ptr.

Is there a tool for this inside the C++11 standard, boost or a popular enough library? I could accept defining my own class for this, but that would be the least preferred method (due to lack of thorough testing).

4 Answers

std::unique_ptr is nullable. It becomes null whenever moved-from, or when default constructed.

std::unique_ptr is your nullable heap allocated object.

A value_ptr can be written that is not nullable. Note that there are extra costs at move:

template<class T>
class value_ptr {
  struct ctor_key_token{ explicit ctor_key_token(int){} };
public:
  template<class A0, class...Args, class dA0 = std::decay_t<A0>,
    std::enable_if_t<!std::is_same<dA0, ctor_key_token>{} && !std::is_same<dA0, value_ptr>{}, int> = 0
  >
  value_ptr( A0&& a0, Args&&... args):
    value_ptr( ctor_key_token(0), std::forward<A0>(a0), std::forward<Args>(args)... )
  {}
  value_ptr(): value_ptr( ctor_key_token(0) ) {}

  template<class X, class...Args>
  value_ptr( std::initializer_list<X> il, Args&&... args ):
    value_ptr( ctor_key_token(0), il, std::forward<Args>(args)... )
  {}

  value_ptr( value_ptr const& o ):
    value_ptr( ctor_key_token(0), *o.state )
  {}
  value_ptr( value_ptr&& o ):
    value_ptr( ctor_key_token(0), std::move(*o.state) )
  {}

  value_ptr& operator=(value_ptr const& o) {
    *state = *o.state;
    return *this;
  }
  value_ptr& operator=(value_ptr && o) {
    *state = std::move(*o.state);
    return *this;
  }

  T* get() const { return state.get(); }
  T* operator->() const { return get(); }
  T& operator*() const { return *state; }

  template<class U,
    std::enable_if_t<std::is_convertible<T const&, U>{}, int> =0
  >
  operator value_ptr<U>() const& {
    return {*state};
  }
  template<class U,
    std::enable_if_t<std::is_convertible<T&&, U>{}, int> =0
  >
  operator value_ptr<U>() && {
    return {std::move(*state)};
  }
private:
  template<class...Args>
  value_ptr( ctor_key_token, Args&&... args):
    state( std::make_unique<T>(std::forward<Args>(args)...) )
  {}

  std::unique_ptr<T> state;
};

that is a rough sketch of a non-nullable heap-allocated value semantics object.

Note that when you move-from it, it doesn't free the old memory. The only time it doesn't own a T on the heap is during construction (which can only abort via a throw) and during destruction (as state is destroyed).

Fancier versions can have custrom destroyers, cloners and movers, permitting polymorphic value semantic types or non-copyable types to be stored.

Using types that are almost-never-null or rarely-null as never-null leads to bugs. So don't do it.

Live example.

It's not possible, in C++'s type system, to write a non-nullable unique_ptr. unique_ptr being nullable is not just convention. This is the point that is badly being missed in many of the comments. What would the move constructor look like? This point has been covered before: https://youtu.be/zgOF4NrQllo?t=38m45s. Since a non-nullable unique_ptr type is not possible, you may as well use unique_ptr pointer in either case.

If you want, you could create a pointer type that is just like unique_ptr, but doesn't have a public default constructor. It would still enter the null state every time it was moved from. This doesn't give you much in the way of guarantees, but it gives you a little, and it serves as documentation. I don't think this type is worth enough to justify its existence.

I suggest simply making it a convention in the codebase that a std::unique_ptr always points to something unless it's a class member being initialized inside a constructor or has just been dereferenced and is about to go out of scope (and only in these cases may it contain null)

Related