8
\$\begingroup\$

I gave a shot at implementing Maybe for C++ and a slight twist in my implementation is that it uses thread_local static instance of Just and Nothing + placement new operator to minimize the number of (de)allocations.

This is the first time I'm using thread_local and placement new operator, so I could be doing something really wrong here. I would appreciate if you could take a look at the code and give your feedback.

Maybe.hpp:

#include <exception>
#include <functional>
#include <memory>

namespace ro {
  /*
   * A class that may or may not hold a value in it.
   */
  template <typename T>
  class Maybe
  {
  public:
      /*
       * A deleter that doesn't actually delete the pointer. This is used to make sure that
       * the thread_local static instance on the stack doesn't get deleted when going out
       * of scope
       */
      struct NoopDeleter
      {
          void operator()(Maybe<T>*) {}
      };
      using pointer_t = std::shared_ptr<Maybe<T>>;

      /*
       * Gets an pointer to a Maybe that's nothing
       */
      static pointer_t nothing();

      /*
       * Gets a pointer to a Maybe that just have a value.
       */
      static pointer_t just(const T& value);

  public:
      Maybe() = default;
      virtual ~Maybe() = default;

      /*
       * Returns if this Maybe is nothing
       */
      virtual bool isNothing() const = 0;

      /*
       * Gets the value, if this instance has one. Throws a runtimer_error otherwise.
       */
      virtual T get() const = 0;

      /*
       * Gets the value held or the passed in value otherwise.
       */
      T getOrElse(const T& defaultValue) const
      {
          if (isNothing())
          {
              return defaultValue;
          }
          return get();
      }

      /*
       * Gets the value stored or throws the exception as supplied by the method passed in
       */
      T getOrThrow(const std::function<std::exception()>& exceptionSupplier) const
      {
          if (isNothing())
          {
              throw exceptionSupplier();
          }
          return get();
      }

      /*
       * Binds a function to convert the stored value (if any) to another of the same type
       */
      pointer_t bind(const std::function < T(const T&)>& func) const
      {
          return map<T>(func);
      }

      /*
       * Binds a function to convert the stored value (if any) to another Maybe of the same type
       */
      pointer_t flatBind(const std::function<pointer_t(const T&)>& func) const
      {
          return flatMap<T>(func);
      }

      /*
       * Maps the current value (if any) to another type.
       */
      template <typename U>
      typename Maybe<U>::pointer_t map(const std::function<U(const T&)>& func) const
      {
          return flatMap<U>([&](const T& val) { return Maybe<U>::just(func(val)); });
      }

      /*
       * Maps the current value (if any) to another type, using the method that returns a
       * Maybe of the mapped type.
       */
      template <typename U>
      typename Maybe<U>::pointer_t flatMap(const std::function<typename Maybe<U>::pointer_t(const T&)>& func) const
      {
          if (isNothing())
          {
              return Maybe<U>::nothing();
          }
          return func(get());
      }
  };

  template <typename T>
  class Nothing : public Maybe<T>
  {
  public:
      virtual bool isNothing() const override
      {
          return true;
      }

      virtual T get() const override
      {
          throw std::runtime_error("No value has been set for this.");
      }
  };

  template <typename T>
  class Just : public Maybe<T>
  {
  public:
      Just(const T& value)
          : mValue(value)
      {
      }

      virtual bool isNothing() const override
      {
          return false;
      }

      virtual T get() const override
      {
          return mValue;
      }

  private:
      const T mValue;
  };

  template <typename T>
  typename Maybe<T>::pointer_t Maybe<T>::nothing()
  {
      thread_local static Nothing<T> nothingInstance;
      static typename Maybe<T>::pointer_t nothing(&nothingInstance, NoopDeleter());
      return nothing;
  }

  template <typename T>
  typename Maybe<T>::pointer_t Maybe<T>::just(const T& value)
  {
      // Found out that thread_local isn't a good idea for 'just'.
      // thread_local static Just<T> justInstance(value);
      // typename Maybe<T>::pointer_t just(new (&justInstance) Just<T>(value), NoopDeleter());
      // return just;
      return std::make_shared<Just<T>>(value);
  }
}

Here's a working example: ideone

\$\endgroup\$
2
  • \$\begingroup\$ Fair point. Admittedly, I really didn't look into the help-center for rules, never have, but I guess I learned my lesson. I think this question isn't off-topic. What I'm specifically looking for is correctness of my code. While I think this might work, I'm not sure of any edge-cases where this may go horribly wrong. \$\endgroup\$ Commented Aug 24, 2015 at 16:08
  • \$\begingroup\$ Aside: There are reviews of a number of optional/maybe-types for C++ on this site. \$\endgroup\$ Commented Oct 23, 2015 at 20:34

1 Answer 1

5
\$\begingroup\$

Allocation

The thing that jumps out at me most is the need for memory allocation to create a Just<T> or Nothing<T>. That's a performance hit, which is why boost::optional and what will eventually be std::optional don't do it this way.

It additionally makes the usage of the class a bit awkward. What I want to do:

Maybe<int> result = foo(x);
if (isNothing(result)) { ... } // or any other way to check

but I have to write:

Maybe<int>::pointer_t result = foo(x);
if (result->isNothing()) { ... }

That's awkward.

get()

You have get() returning by value, this incurs unnecessary copies at best, but if T isn't copyable makes Just<T> useless. You should instead prefer:

virtual T& get() = 0;
virtual T const& get() const = 0;

Returning T for getOrElse() makes sense though - as you may want to support the else case as a temporary. On the other hand, getOrThrow() will only ever return get() so it should return a reference.

std::function is for type erasure

Throughout, you use std::function. But std::function is for type erasure. It's for those cases where you need to store a functor. In none of your usages do you need this feature.

At the most basic, take getOrThrow(). Just take it as template argument:

template <typename F>
T& getOrThrow(F&& exceptionSupplier) {
    if (isNothing()) {
        throw std::forward<F>(exceptionSupplier)();
    }
    return get();
}

template <typename F>
T const& getOrThrow(F&& exceptionSupplier) const {
    return const_cast<Maybe*>(this)->getOrThrow(std::forward<F>(exceptionSupplier));
}

If you want to add more SFINAE goodness here, you could additionally require that exceptionSupplier() gives you something that inherits from std::exception.

But this is just an overhead thing. Your implementations of map, flatMap, bind, and flatBind all take as arguments a std::function of some sort too. This is ok but inefficient for bind and flatBind, but makes map and flatMap much less usable:

Maybe<int>::pointer_t result = ...;
auto add1 = result->flatMap([](int i){ return just(i+1); }); // error

because I have to write it like:

auto add1 = result->flatMap<int>([](int i){ return just(i+1); }); // OK but blargh!

Prefer something like:

template <typename F,
          typename R = decltype(std::declval<F>()(std::declval<T&>()), // this is your Maybe<U>::pointer_t
          typename MaybeU = typename R::element_type // this is your Maybe<U>
          >
R flatMap(F&& func)
{
    return isNothing()
        ? MaybeU::nothing()
        : std::forward<F>(func)(get());
}

This would let users call your functions with raw lambdas - which is what they'd expect to be able to use!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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