Adding an optional_ptr to my tool box
Smartpointer, very useful.
They exists since ever, for example, in a useful form I know about in boost since 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,
what I use less often, but every once and than 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 say clear it is some not owned pointer that is guaranteed not null. This improves readability of code 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 pointer in code I have to review.
Every time I see one I have to check that this is used correct. That this raw pointer I see should not be a unique, shared or a not_null pointer.
I have to do this, with code I have written my own days ago and I have forgotten the implementation details.
Or with code I have to review, even from developers from whom I think that they are very skilled and know what the 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
A optional pointer is a non owning pointer and 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 a pointers should not produce much of 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 again 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 if it holds a pointer
-
throws an exception in case of nullptr access
-
remove unwanted pointer operations, like ptr++, ptr-- …
-
So, lets 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/inlcude
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 unsusable
}
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 unsusable
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{notnull_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_shared == -42)
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 ad tomorrow this into my toolbox.
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 implementer such a 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 int TS2. Nice to have now 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.