Motivation
This is intended to implement a C++ maybe_ptr
type that can either hold a pointer to an underlying type T
or a nullptr
. The idea is to guarantee at compile-time the absence of null pointer dereferencing errors in code that uses raw pointers.
Traditionally, these kinds of errors are prevented by the user having to guard pointer dereferencing by explicitly checking nullptr
. For example:
void foo(int* n)
{
if(n != nullptr)
{
bar(*n);
}
}
The obvious danger is that the user forgets to check for nullptr
, in which case it is undefined behavior that is not detected by the compiler and may not even be easily detected at run-time.
In contrast, maybe_ptr
guarantees nullptr
checking before the user is able to deference the raw pointer.
Usage
Before getting to the implementation, let's see how maybe_ptr
is used (leveraging C++11 lambdas):
void foo(const maybe_ptr<int>& ptr)
{
bool ran = maybe_if(ptr, [](int* n)
{
bar(*n); // never reached if n is null
});
// ran is true if ptr did not contain null (i.e. the lambda was run)
}
We can easily assign raw pointers to initialize maybe_ptr
:
int n;
maybe_ptr<int> ptr = &n;
Note that maybe_ptr
does not allow any direct access to the raw pointer once it is assigned:
maybe_ptr<std::string> ptr;
std::string* s = *ptr; // compiler error: no "*" operator defined
ptr->length(); // compiler error: no "->" operator defined
ptr->get(); // compiler error: "get" is inaccessible (private method)
The only way to access the pointer is through the maybe_if
function, with a function pointer, lambda function, or functor. A couple overloaded maybe_if
functions are provided for convenient, simultaneous access to multiple pointers within the same lambda function:
void foo(const maybe_ptr<int>& ptr1, const maybe_ptr<float>& ptr2)
{
bool ran = maybe_if(ptr1, ptr2, [](int* n, float* x)
{
bar(*n, *x); // never reached if n or x are null
});
// ran is true if ptr1 & ptr2 did not contain null
}
An additional function called maybe_iff
is provided, if we want to just throw an exception on nullptr
:
void foo(const maybe_ptr<int>& ptr)
{
// throws exception if ptr contains null
maybe_iff(ptr, [](int* n)
{
bar(*n); // never reached if n is null
});
}
Finally, for additional safety, we also disable the copy constructor in maybe_if
(similar to how std::unique_ptr
cannot be copied):
maybe_if<int> ptr1;
maybe_if<int> ptr2 = ptr1; // compiler error: copy constructor is deleted
Implementation
#pragma once
/// <summary>
/// Non-allocating "Maybe" type for raw pointers. The purpose of maybe_ptr
/// is to prevent null pointer dereferencing by wrapping the raw pointer and forcing
/// all access to the raw pointer via a functor passed to maybe_if or maybe_iff
/// </summary>
/// <remarks>
/// maybe_ptr does not own the underlying raw pointer (i.e. it does not manage its memory)
/// maybe_ptr cannot prevent dangling pointer errors, it only checks for nullptr
/// </remarks>
/// <typeparam name="T">the underlying type of the raw pointer</typeparam>
template <typename T>
class maybe_ptr {
public:
/// <summary>
/// Construct new maybe_ptr
/// </summary>
maybe_ptr() : _ptr(nullptr), _isNotNull(false) { }
/// <summary>
/// Construct maybe_ptr from a raw pointer. maybe_ptr does not manage the
/// memory of the pointer, it only restricts access to the pointer
/// </summary>
/// <param name="other">raw pointer to wrap with maybe_ptr</param>
maybe_ptr(T* other)
{
set(other);
}
/// <summary>
/// Disable copy constructor to minimize risk dangling pointers
/// </summary>
maybe_ptr(const maybe_ptr&) = delete;
/// <summary>
/// Assignnment operator from raw pointer to this maybe_ptr. Previous raw pointer is overwitten.
/// </summary>
/// <param name="other">raw pointer to wrap with maybe_ptr</param>
/// <returns></returns>
maybe_ptr<T>& operator=(T* other)
{
set(other);
return *this;
}
/// <summary>
/// Bool operator to check if underlying raw pointer is not null
/// </summary>
/// <returns>true if maybe_ptr is not null</returns>
explicit operator bool() const
{
return _isNotNull;
}
private:
/// <summary>
/// Return raw pointer
/// </summary>
/// <returns>underlying raw pointer</returns>
T* get() const
{
return _ptr;
}
/// <summary>
/// Set the underlying raw pointer and check if it is null
/// </summary>
/// <param name="ptr">raw pointer to wrap with this maybe_ptr</param>
void set(T* ptr)
{
_ptr = ptr;
_isNotNull = ptr != nullptr;
}
T* _ptr;
bool _isNotNull;
template<typename T, typename FunctorT>
friend bool maybe_if(const maybe_ptr<T>& ptr, typename FunctorT functor);
template<typename T1, typename T2, typename FunctorT>
friend bool maybe_if(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, typename FunctorT functor);
template<typename T1, typename T2, typename T3, typename FunctorT>
friend bool maybe_if(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, const maybe_ptr<T3>& ptr3, typename FunctorT functor);
template<typename T, typename FunctorT>
friend void maybe_iff(const T& ptr, typename FunctorT functor);
template<typename T1, typename T2, typename FunctorT>
friend void maybe_iff(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, typename FunctorT functor);
template<typename T1, typename T2, typename T3, typename FunctorT>
friend void maybe_iff(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, const maybe_ptr<T3>& ptr3, typename FunctorT functor);
};
/// <summary>
/// Run functor on ptr if ptr is not null, otherwise do nothing
/// </summary>
/// <param name="ptr">maybe_ptr to run functor on</param>
/// <param name="functor">function to if ptr is not null</param>
/// <returns>true if functor ran, false if it did not run</returns>
template<typename T, typename FunctorT>
bool maybe_if(const maybe_ptr<T>& ptr, typename FunctorT functor)
{
if (ptr)
{
functor(ptr.get());
return true;
}
return false;
}
template<typename T1, typename T2, typename FunctorT>
bool maybe_if(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, typename FunctorT functor)
{
if (ptr1 && ptr2)
{
functor(ptr1.get(), ptr2.get());
return true;
}
return false;
}
template<typename T1, typename T2, typename T3, typename FunctorT>
bool maybe_if(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, const maybe_ptr<T3>& ptr3, typename FunctorT functor)
{
if (ptr1 && ptr2 && ptr3)
{
functor(ptr1.get(), ptr2.get(), ptr3.get());
return true;
}
return false;
}
/// <summary>
/// Run functor on ptr if ptr is not null, otherwise throw exception
/// </summary>
/// <param name="ptr">maybe_ptr to run functor on</param>
/// <param name="functor">function to if ptr is not null</param>
/// <returns>true if functor ran, false if it did not run</returns>
template<typename T, typename FunctorT>
void maybe_iff(const T& ptr, typename FunctorT functor)
{
THROW_ASSERT(ptr, "Unexpected null maybe_ptr") // custom assert macro throws exception
functor(ptr.get());
}
template<typename T1, typename T2, typename FunctorT>
void maybe_iff(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, typename FunctorT functor)
{
THROW_ASSERT(ptr1 && ptr2, "Unexpected null maybe_ptr")
functor(ptr1.get(), ptr2.get());
}
template<typename T1, typename T2, typename T3, typename FunctorT>
void maybe_iff(const maybe_ptr<T1>& ptr1, const maybe_ptr<T2>& ptr2, const maybe_ptr<T3>& ptr3, typename FunctorT functor)
{
THROW_ASSERT(ptr1 && ptr2 && ptr3, "Unexpected null maybe_ptr")
functor(ptr1.get(), ptr2.get(), ptr3.get());
}
Issues
The implementation has to explicitly define overloaded versions of
maybe_if
andmaybe_iff
for differing numbers of arguments. Currently only overloaded version for one, two or threemaybe_ptr
arguments are defined. It would be nice to replace this with C++11 variadic templates, but I ran into template argument deduction problems when I attempted to use variadic templates.In a similar vein, it might be nice to eliminate the
FunctorT
template parameter inmaybe_if
, and explicitly define thefunctor
argument type asstd::function<void(T*)>
,std::function<void(T1*, T2*)>
, etc., however it appears that C++11 lambdas do not work in that case.The
functor
seemingly has no way of returning any values. Themaybe_if
cannot return values from thefunctor
argument (it is assumed to have void return type). Thefunctor
also cannot have any additional arguments through which values could be returned. However, this is not a huge deal because we can use lambda capture to assign values to variables in the enclosing scope.The
maybe_if
usage is more verbose and syntactically awkward that traditional if-statement include guards. The becomes particularly acute when we have to do nested calls to maybe_if.The functions
maybe_if
andmaybe_iff
and very similarly named, even though they have very different semantics. An alternative would be to renamemaybe_iff
tomaybe_if_assert
ormaybe_if_throw
but I decided not to do that in order to avoid making the API more verbose than it already is.Use of
maybe_if
adds the overhead of a conditional branch in every call tomaybe_if
. The overhead can be amortized over multiple statements by including them within the same call tomaybe_if
, but the overhead still exists. However, I estimate that it is extremely minimal and worth the added safety.
Note: This idea was inspired by a different Maybe
type implementation described here (full source)
constexpr
in code, are you sure that the specification is correct? \$\endgroup\$ – Incomputable Feb 10 '17 at 10:39