Mix boost::optional and std::unique_ptr
So to recap your question, you want:
- A non-optional type that is allocated by value/on the stack: You are happy directly using the object type for this.
- An optional type that is allocated by value/on the stack: You are happy using
boost::optional
for this (or you can usestd::optional
from C++17). - A non-optional type that is allocated on the heap and owns the pointed-to object.
- 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.