HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

An attempt at implementing Maybe in C++11

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
maybeimplementingattempt

Problem

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
#include
#include

namespace ro {
/*
* A class that may or may not hold a value in it.
*/
template
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*) {}
};
using pointer_t = std::shared_ptr>;

/*
* 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& exceptionSupplier) const
{
if (isNothing())
{
throw exceptionSupplier

Solution

Allocation

The thing that jumps out at me most is the need for memory allocation to create a Just or Nothing. 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 result = foo(x);
if (isNothing(result)) { ... } // or any other way to check


but I have to write:

Maybe::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 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 
T& getOrThrow(F&& exceptionSupplier) {
    if (isNothing()) {
        throw std::forward(exceptionSupplier)();
    }
    return get();
}

template 
T const& getOrThrow(F&& exceptionSupplier) const {
    return const_cast(this)->getOrThrow(std::forward(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::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 i){ return just(i+1); }); // OK but blargh!


Prefer something like:

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


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

Code Snippets

Maybe<int> result = foo(x);
if (isNothing(result)) { ... } // or any other way to check
Maybe<int>::pointer_t result = foo(x);
if (result->isNothing()) { ... }
virtual T& get() = 0;
virtual T const& get() const = 0;
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));
}
Maybe<int>::pointer_t result = ...;
auto add1 = result->flatMap([](int i){ return just(i+1); }); // error

Context

StackExchange Code Review Q#101769, answer score: 5

Revisions (0)

No revisions yet.