Mix boost::optional and std::unique_ptr

So to recap your question, you want:

  1. A non-optional type that is allocated by value/on the stack: You are happy directly using the object type for this.
  2. An optional type that is allocated by value/on the stack: You are happy using boost::optional for this (or you can use std::optional from C++17).
  3. A non-optional type that is allocated on the heap and owns the pointed-to object.
  4. An optional type that is allocated on the heap and owns the pointed-to object.

You are unhappy that you can express the difference between 1 and 2, but both 3 and 4 usually use the same type (std::unique_ptr). You suggest using std::unique_ptr for 3, never allowing nullptr, and some other thing for 4, but want to know what you can use. (In the comments you also accept the possibility of using std::unique_ptr with nullptr for 4 if something else can be found for 3.)

Literal answer to your question: you can simply use boost::optional<std::unique_ptr<T>> for 4 (while using a bare unique_ptr for 3 as you suggested).

Alternative literal answer to your question: As @StoryTeller said, you could define your own smart pointer type that is like unique_ptr but disallows nullptr, and use that for 3. A quicker (but very dirty) alternative is to force functions to return a pair of both a unique_ptr and a reference to that same object. Then only access the result through the reference, but only do so while the unique_ptr still exists:

template<class T>
using RefAndPtr = std::pair<T&, std::unique_ptr<T>>;

RefAndPtr<Foo> getFoo()
{
    std::unique_ptr<Foo> result = std::make_unique<Foo>();
    return RefAndPtr<Foo>(*result, std::move(result));
}

My actual suggestion: Just suck it up and use std::unique_ptr for both 3 and 4. Clarifying your intentions in the type system is a good thing, but too much of a good thing can be bad. Using either of the above options is just going to confuse the hell out of anyone that reads your code. And even if you stop people from incorrectly passing around nullptr, what's to stop them passing a pointer around to the wrong object, or already-freed memory, etc.? At some point you have to specify things outside of the type system.


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.