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
-
\$\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\$Vite Falcon– Vite Falcon2015年08月24日 16:08:23 +00:00Commented Aug 24, 2015 at 16:08
-
\$\begingroup\$ Aside: There are reviews of a number of optional/maybe-types for C++ on this site. \$\endgroup\$Deduplicator– Deduplicator2015年10月23日 20:34:25 +00:00Commented Oct 23, 2015 at 20:34
1 Answer 1
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!