8

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).

user2891462
  • 3,033
  • 2
  • 32
  • 60
  • 4
    Being explicit is nice. But there's also convention to consider. Pointers are nullable types since time immemorial. – StoryTeller - Unslander Monica Sep 19 '17 at 11:48
  • So you want a non-nullable analogue of `unique_ptr`, is this correct? – n. m. could be an AI Sep 19 '17 at 11:50
  • @StoryTeller Yes, but sometimes you don't have the pointer for the nullability but other reasons. In those cases, I find it useful to be able to convery it somehow to save useless checks populating the code. – user2891462 Sep 19 '17 at 11:51
  • 1
    @StoryTeller The inventor of null pointers has called his invention a "billion dollar mistake", perhaps it's about time to fix it. – n. m. could be an AI Sep 19 '17 at 11:51
  • @n.m. - Like all technical debt, highly unlikely. Even more so in this case, I'd wager. – StoryTeller - Unslander Monica Sep 19 '17 at 11:52
  • @n.m. I was looking for a type that I would use in my codebase as "nullable unique pointer" so that I can treat plain "unique pointer" as non-nullable from then on. Your idea would work too. – user2891462 Sep 19 '17 at 11:52
  • @n.m. Such a thing by definition can't be a [Regular type](http://stepanovpapers.com/DeSt98.pdf), which restricts what other code it can interact with – Caleth Sep 19 '17 at 11:55
  • 1
    `unique_ptr` is nullable, you cannot treat it as non-nullable. – n. m. could be an AI Sep 19 '17 at 12:01
  • 1
    I don't think the correct approach is to use optional, TBH. Rely on the convention to signal nullability, return a unique_ptr. And define a *new* smart pointer type (possibly by inheriting from unique_ptr), than enforces a non-null invariant. That way, when you see the new type in your code base, you know there's no need to check, and when you see the old type, the convention takes care of warning the user. – StoryTeller - Unslander Monica Sep 19 '17 at 12:02
  • @n.m. I know it's nullable, and I operate under that assumption when dealing with someone else's project. For my own projects, however, I prefer to stick to my own convention, which states that if a `unique_ptr` can be null, it shouldn't be a `unique_ptr` but something else. I understand this might not work for everyone, but it gave me very good results when I had to use it in a few projects years ago (this had been the original developer's idea). – user2891462 Sep 19 '17 at 12:07
  • @Caleth I don't see how not having a default constructor restricts anything. – n. m. could be an AI Sep 19 '17 at 12:08
  • 1
    If your convention works for you, what else do you need? A nullable pointer? Here: `template using nullable_unique_ptr = unique_ptr;` This type is **by convention** nullable. – n. m. could be an AI Sep 19 '17 at 12:11
  • @n.m. all the template code that default constructs objects of type parameters. If you have the invariant that it can't be null, then you can't move from it, either. – Caleth Sep 19 '17 at 12:11
  • @Caleth "all the template code that default constructs objects of type parameters" is uninteresting and broken, I don't care about any such code. Since moving-from leaves the object in a destructible but otherwise indeterminate state, have a null value that only moving-from can create. You are not supposed to use a moved-from object for anything anyway. – n. m. could be an AI Sep 19 '17 at 12:18
  • @n.m. let me rephrase: Such a type cannot be used in any template that requires that type parameters to be one of DefaultConstructible or CopyConstructible (or, I contend, MoveConstructible). – Caleth Sep 19 '17 at 12:34
  • @Caleth "Such a type cannot be used in any template that requires that type parameters to be one of DefaultConstructible" Nothing of value is lost. "or CopyConstructible" The regular `unique_ptr` is not CopyConstructible either, so? "or, I contend, MoveConstructible" This remains to be seen. I don't know of any standard library class or function template that wouldn't work with my proposed semantics of moving-from (except `std::swap` but that should be specialised anyway). – n. m. could be an AI Sep 19 '17 at 12:53
  • @n.m. the general requirement of moved-from objects is that they are in a valid state, i.e. that operations with no prerequisites can be applied to them. The point of a `not_null` pointer is to remove the "isn't null" prerequisite from `operator*` et al, so we are back to "have to check for null" or "isn't MoveConstructible" – Caleth Sep 19 '17 at 13:00
  • @Caleth "operations with no prerequisites can be applied to them" Your definition of "valid state" is overly restrictive. Why an operation on a template parameter type that the template doesn't know about and never invokes should be required to succeed? Or, if you prefer, "not moved-from" is a prerequisite for dereferencing (a prerequisite need not be a Boolean C++ expression, compare with the "pointer is valid" prerequisite for most operations on plain pointers --- it must hold but you cannot check for it). – n. m. could be an AI Sep 19 '17 at 13:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/154800/discussion-between-caleth-and-n-m). – Caleth Sep 19 '17 at 13:33
  • Possible duplicate of [With std::optional standardizing, can we stop using nullptr in new code and deprecate it?](https://stackoverflow.com/questions/43044464/with-stdoptional-standardizing-can-we-stop-using-nullptr-in-new-code-and-depr) – Nir Friedman Sep 19 '17 at 17:55

4 Answers4

6

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.

Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49
  • Your recap is correct (though I would add that the object in 3 and 4 needs to have clear ownership rules, so no raw pointer). I had thought of `boost::optional>`, but thought it would be too confusing (plus it would be great to have the internal unique_ptr abstracted away in this new class, returning a reference to the internal object if it exists), and I wasn't sure `boost::optional` would be able to deal with the subtleties of `unique_ptr` either. – user2891462 Sep 19 '17 at 12:11
  • 1
    Good point, I updated 3 and 4 to mention ownership. I also added a quick hack for a non-nullable `unique_ptr`-like object, but it's very dirty! – Arthur Tacca Sep 19 '17 at 12:17
  • 2
    You could also use `gsl::not_null>` as a convention. – Caleth Sep 19 '17 at 12:41
  • @Caleth: I though than `gsl::not_null` doesn't support `std::unique_ptr`... – Jarod42 Sep 19 '17 at 17:42
  • @Caleth It's worth adding that as a separate answer. – Arthur Tacca Sep 20 '17 at 08:39
2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Of course, `struct S { int m[1000]; }; using Sptr = value_ptr;` will always be terrible. – aschepler Sep 19 '17 at 22:36
  • @aschepler if you ensure all moves are elisions, and/or maybe add a swap specialization, you might be able to keep it from being garbage. We could also improve `=(&&)` with swap now that I think of it. – Yakk - Adam Nevraumont Sep 19 '17 at 22:54
0

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.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • It could be possible to have something like a `shared_ptr` without a `nullptr`. That would work, although it would have extra runtime overhead. – Arthur Tacca Sep 30 '17 at 11:48
0

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)

claz78
  • 46
  • 1
  • 4