Adding an optional_ptr to my toolbox

Smart pointers, very useful.

They have existed forever; for example, in a useful form I have known about in boost for very many years.
With C++11 we got them into the standard.

The smart pointers we know

std::unique_ptr

What I use most often.

std::shared_ptr,

which I use less often, but every once and then there is a use case that makes code convenient.

gsl::not_null

The not_null pointer from the Guideline Support Library

I like this pointer a lot, it removes a lot of raw pointers, especially as function arguments. This type clearly says it is some non-owned pointer that is guaranteed not null. This improves code readability a lot.

I am unsure if I can name gsl::not_null a smart pointer, or just a wrapper, but for now I will not create a new category.

A (smart)pointer type I miss

There are still raw pointers in code I have to review.

Every time I see one I have to check that this is used correctly. That this raw pointer I see should not be a unique_ptr, shared_ptr or a not_null pointer.

I have to do this with code I have written myself days ago and have forgotten the implementation details. Or with code I have to review, even from developers whom I think are very skilled and know what they do.
It is a rule, and it is a very good rule.

But it is somehow tedious. That makes me think about creating an additional pointer class to eliminate this type of raw pointer.

Such a pointer is not a unique, shared, or not_null pointer. It is a raw pointer that might, or might not, be a nullptr. An optional pointer.

Therefore I call it optional_ptr.

optional_ptr

An optional pointer is a non-owning pointer that might be a nullptr.

Using such a pointer wrapper should eliminate the rest of the raw pointers we have in the code (well…​ but that’s a blog for the future :-)

Such pointers should not produce much additional overhead and should - and this is the most important part for me - make reading code more fluent, easy and clear.

I accept happily the overhead to check for a nullptr access and throw an exception in this case. This is a design decision since raw pointers have to be checked against null before usage anyway to satisfy static analysis tools.

Let’s see how such an optional_ptr might look like
  • Can be constructed from a

    • raw pointer

    • unique pointer

    • shared pointer

    • not null pointer

    • nullptr

  • can be queried whether it holds a pointer

  • throws an exception in case of nullptr access

  • remove unwanted pointer operations, like ptr++, ptr-- …​

So, let’s add this to the toolbox and see:
How does it work and how will it be accepted by other developers.
I am very curious.

The implementation

#include <cassert>
#include <memory>
#include <stdexcept>

#include <gsl/gsl> // -I/where/ever/Microsoft/GSL/include


template <typename T>
class optional_ptr
{

public:

  struct nullptr_access : public std::runtime_error
  {
    nullptr_access()
    : std::runtime_error{"optional_ptr::nullptr_access"}
    {
    }
  } ;

  using pointer = T* ;
  using reference = T& ;


  constexpr optional_ptr() noexcept
  :_p{nullptr}
  {
  }

  explicit optional_ptr(pointer p) noexcept
  :_p{p}
  {
  }

  explicit optional_ptr(const std::unique_ptr<T>& p) noexcept
  :_p{p.get()}
  {
  }

  explicit optional_ptr(const std::shared_ptr<T>& p) noexcept
  :_p{p.get()}
  {
  }

  explicit optional_ptr(const gsl::not_null<pointer>& p) noexcept
  :_p{p.get()}
  {
  }


  ~optional_ptr() noexcept = default ;

  optional_ptr(optional_ptr& ) noexcept = default ;

  optional_ptr(optional_ptr&& other) noexcept
  :_p(std::move(other._p))
  {
    other.reset(); // set other into something not unusable
  }

  optional_ptr& operator=(optional_ptr&) noexcept = default ;

  optional_ptr& operator=(optional_ptr&& other) noexcept
  {
    _p = std::move(other._p) ;
    other.reset(); // set other into something not unusable
    return *this ;
  }


  reference
  operator*() const
  {
    return *get();
  }

  pointer
  operator->() const
  {
    return get();
  }

  pointer
  get() const
  {
    if(_p == nullptr)
      throw nullptr_access() ;

    return _p ;
  }

  void
  reset() noexcept
  {
    reset(nullptr);
  }

  void
  reset(pointer p) noexcept
  {
     _p = p ;
  }

  explicit operator bool() const noexcept
  {
    return _p == nullptr ? false : true;
  }

  bool operator==(std::nullptr_t) const noexcept
  {
    return _p == nullptr ;
  }

  bool operator!=(std::nullptr_t) const noexcept
  {
    return !(*this == nullptr) ;
  }

private:
  pointer _p;

  // unwanted operators...pointers only point to single objects!
  optional_ptr<T>& operator++() = delete;
  optional_ptr<T>& operator--() = delete;
  optional_ptr<T> operator++(int) = delete;
  optional_ptr<T> operator--(int) = delete;
  optional_ptr<T>& operator+(size_t) = delete;
  optional_ptr<T>& operator+=(size_t) = delete;
  optional_ptr<T>& operator-(size_t) = delete;
  optional_ptr<T>& operator-=(size_t) = delete;

};

int main(int , char** )
{
  auto shared_int = std::make_shared<int>(-42) ;
  auto unique_int = std::make_unique<int>(42) ;
  int* raw_int = new int{123} ;
  gsl::not_null<int*> not_null_int{raw_int} ;

  optional_ptr<int> from_shared{shared_int} ;
  optional_ptr<int> from_unique{unique_int} ;
  optional_ptr<int> from_notnull{not_null_int} ;
  optional_ptr<int> from_raw{raw_int} ;
  optional_ptr<int> from_null{nullptr} ;

  {
    bool check_passed{false} ;
    if(from_shared && *from_shared == -42)
      check_passed = true ;
    assert(check_passed) ;
  }

  {
    bool check_passed{false} ;
    if(from_unique && *from_unique == 42)
      check_passed = true ;
    assert(check_passed) ;
  }

  {
    bool check_passed{false} ;
    if(from_notnull && *from_notnull == 123)
      check_passed = true ;
    assert(check_passed) ;
  }

  {
    bool check_passed{false} ;
    if(from_raw && *from_raw == 123)
      check_passed = true ;
    assert(check_passed) ;
  }

  {
    bool check_passed{false} ;
    try
    {
      if( *from_null == 0)
        check_passed = false ;
    }
    catch (const std::runtime_error&)
    {
      check_passed = true ;
    }
    assert(check_passed) ;
  }

  delete raw_int ;
  return 0 ;
}

Some things to note

Type declarations

gsl::not_null<int*>
while
std::unique_ptr<int> or _ std::shared_ptr<int>

I follow std naming and use optional_ptr<int>

Adopting the gsl::not_null is not hard, so I will add this to my toolbox tomorrow.

template <typename T>
using notnull_ptr = gsl::not_null<T*>;

This will allow me to use notnull_ptr<int> and so I have consistency in naming.

Also, I am pretty sure that my implementation might need some improvements for the one or other detail which is currently unknown to me. If you see something that is wrong or need to be added, feedback is appreciated.

I am also nearly sure that I am not the first person who has implemented such an optional_ptr, maybe under a different name. If you know such implementations, please let me know.

Update

Thanks a lot to chris (real name unknown) from cpplang slack chat for pointing me to the observer_ptr in TS2. Nice to have already a drop-in replacement
Code (blog) review via internet is great!

As usual

If you find any mistakes, in the code or in my spelling, please add a comment below, ideas for improvements are also very much welcome.