6
\$\begingroup\$

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

  1. The implementation has to explicitly define overloaded versions of maybe_if and maybe_iff for differing numbers of arguments. Currently only overloaded version for one, two or three maybe_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.

  2. In a similar vein, it might be nice to eliminate the FunctorT template parameter in maybe_if, and explicitly define the functor argument type as std::function<void(T*)>, std::function<void(T1*, T2*)>, etc., however it appears that C++11 lambdas do not work in that case.

  3. The functor seemingly has no way of returning any values. The maybe_if cannot return values from the functor argument (it is assumed to have void return type). The functor 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.

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

  5. The functions maybe_if and maybe_iff and very similarly named, even though they have very different semantics. An alternative would be to rename maybe_iff to maybe_if_assert or maybe_if_throw but I decided not to do that in order to avoid making the API more verbose than it already is.

  6. Use of maybe_if adds the overhead of a conditional branch in every call to maybe_if. The overhead can be amortized over multiple statements by including them within the same call to maybe_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)

\$\endgroup\$
  • 2
    \$\begingroup\$ "The idea is to guarantee at compile-time the absence of null pointer dereferencing errors in code that uses raw pointers.". There is no constexpr in code, are you sure that the specification is correct? \$\endgroup\$ – Incomputable Feb 10 '17 at 10:39
3
\$\begingroup\$

Variadic expansion

Until we get fold expressions, we have to use a workaround to check all the pointers

template<typename ... Args, typename Func>
bool maybe_if(const maybe_ptr<Args> &... args, Func function)
{
  bool result = true;
  (void)std::initializer_list<int>{ (result = result && args, 0)... };

    if (result)
    {
      function(args.get()...);
    }
    return result;
}

Why return bool (or throw)?

The usefulness of an maybe type is more limited when you don't return a maybe<result>.

Instead of your

bool ran = maybe_if(ptr, bar);
if (ran) 
{ 
    bar happened, do other stuff
    /* why didn't we just if (ptr) { bar(*ptr) } here? */ 
}

Instead it is

auto bar_res = maybe_if(ptr, bar);
auto baz_res = maybe_if(bar_res, baz);
// etc...
bool last_res = maybe_if(baz_res, last);
return last_res;

Doing that would resolve points 3-5, however you are explicitly non-owning

Don't use std::function

Your worry that extra overhead will occur from ifs is misplaced, you should be much more worried about hiding functions in std::function<>, which is a very heavyweight construction, which is much more suitable for storing Callables

\$\endgroup\$
  • \$\begingroup\$ why is std::function<> heavyweight? Are you talking about generally or in this particular context? \$\endgroup\$ – Abhinav Gauniyal Feb 10 '17 at 16:06
  • \$\begingroup\$ in comparison to a template parameter, which can easily be inlined, std::function<> hides the implementation. \$\endgroup\$ – Caleth Feb 10 '17 at 16:11
  • \$\begingroup\$ @AbhinavGauniyal, I believe that main reason is that it uses type erasure, which potentially prevents optimizations done by compiler. Current std::function copies arguments, and (if it exceeds small object optimization's size) will make heap allocation. Then function gets out of scope, it will be freed. The free might not be immediate system call, but allocation probably will, which is expensive in performance sensitive code. Also, cache coherence comes into play, but it is more low level issue. \$\endgroup\$ – Incomputable Feb 10 '17 at 19:21
  • \$\begingroup\$ Thanks. The tip on variadic templates and std::function is good to know. \$\endgroup\$ – n00b101 Feb 11 '17 at 0:28
  • \$\begingroup\$ Regarding "Why return bool?", as you noted, this is problematic if the Maybe type does not own the underlying pointer. One of the design objectives of mymaybe_ptr implementation was to safely handle raw pointers returned by third-party library. The libraries in question retain ownership of the pointers, so it is not possible for maybe_ptr to own them. The only usefulness of this maybe_ptr is that it allows the type system to enforce pointer nullptr checking before pointer access, whereas if we just use if (ptr) { bar(*ptr) } then the nullptr checking is not enforced. \$\endgroup\$ – n00b101 Feb 11 '17 at 0:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.