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(¬hingInstance, 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