3
\$\begingroup\$

This supposed to implement a Maybe type that can either hold a value T or "nothing". It's designed to work well with vectors (that's why I take extra care to noexcept constructors whenever possible). I'm governing the actual data with ::std::aligned_storage. Note: The just member function is a reference to the Haskell Maybe data-constructor Just.

Did I miss anything?

#pragma once

#include <utility>
#include <algorithm>
#include <type_traits>

namespace ads {
  template <typename T>
  class Maybe {
    public:
      typedef typename ::std::aligned_storage<sizeof(T),alignof(T)>::type storage_type;
      typedef T value_type;
    private:
      storage_type data;
      bool is_valid;
      inline T* ptr() {
        return reinterpret_cast<T*>(&data);
      }
      inline T const* ptr() const {
        return reinterpret_cast<T const*>(&data);
      }
    public:
      Maybe() noexcept : data(), is_valid(false) {}
      template <typename U>
      Maybe(U&& v) noexcept(::std::is_nothrow_move_constructible<T>::value) : data(), is_valid(false) {
        create(::std::forward<U>(v));
      }
      Maybe(Maybe const& from) noexcept(::std::is_nothrow_copy_constructible<T>::value) : data(), is_valid(false) {
        if (from.is_valid)
          create(*from.ptr());
      }
      Maybe(Maybe&& from) noexcept(::std::is_nothrow_move_constructible<T>::value) : data(), is_valid(false) {
        if (from.is_valid)
          create(::std::move(*from.ptr()));
      }
      template <typename U>
      Maybe& operator=(U&& v) noexcept(::std::is_nothrow_move_constructible<T>::value && ::std::is_nothrow_copy_constructible<T>::value && ::std::is_nothrow_assignable<T,U>::value) {
        create(::std::forward<U>(v));
        return *this;
      }
      Maybe& operator=(Maybe const& from) noexcept(::std::is_nothrow_copy_constructible<T>::value && ::std::is_nothrow_assignable<T,T>::value) {
        if (from.is_valid) {
          create(*from.ptr());
        } else {
          destroy();
        }
        return *this;
      }
      Maybe& operator=(Maybe&& from) noexcept(::std::is_nothrow_move_constructible<T>::value && ::std::is_nothrow_assignable<T,T>::value) {
        if (from.is_valid) {
          create(::std::move(*from.ptr()));
        } else {
          destroy();
        }
        return *this;
      }
      ~Maybe() noexcept(::std::is_nothrow_destructible<T>::value) {
        destroy();
      }

      bool valid() const noexcept {
        return is_valid;
      }
      operator T&() noexcept {
        return *ptr();
      }
      operator T const&() const noexcept {
        return *ptr();
      }
      T& just() noexcept {
        return *ptr();
      }
      T const& just() const noexcept {
        return *ptr();
      }

      template <typename U>
      void create(U&& v) noexcept(::std::is_nothrow_move_constructible<T>::value && ::std::is_nothrow_copy_constructible<T>::value && ::std::is_nothrow_assignable<T,U>::value){
        if (is_valid) {
          *ptr() = ::std::forward<U>(v);
        } else {
          new(&data) T(::std::forward<U>(v));
          is_valid = true;
        }
      }
      void destroy() noexcept(::std::is_nothrow_destructible<T>::value) {
        if (is_valid) {
          ptr()->~T();
          is_valid = false;
        }
      }
  };
}
\$\endgroup\$
  • 2
    \$\begingroup\$ Maybe is a long forgotten name of the std::optional. I would advocate that optional is much better name rather than Maybe. Maybe sounds rather humorous. \$\endgroup\$ – Incomputable Nov 24 '16 at 15:54
  • \$\begingroup\$ Hmpf, okay, at least I learned something, here :) \$\endgroup\$ – bitmask Nov 24 '16 at 17:19
2
\$\begingroup\$

Creating holes in the type system:

operator T& and it's const sibling is probably very undesirable conversion. Why, then, std::string doesn't have operator const char*()? The answer is a type system. It meant to be explicit. std::optional uses overloading of operator*() and operator->().

Don't pay for what you don't use:

I would expect constructor to not check for is_valid of itself, because it is obvious (you're calling create() from constructor, which does check for validity). In most cases this won't make a difference, but you still should strive for perfection.

Missing features:

  • swap
  • comparison operators
  • emplace
  • value_or, which is I think the most useful. Returns provided default value if there optional is not initialized yet.
  • one function that throws when the optional is not initialized yet. just() is a great candidate.

Style:

You could enter a newline in some places where the line is long.

I think that calling global std might be somewhat restricting. It is arguable, though.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your feedback! The bug however, I do not see. In case the object is valid, then I simply assign the new one, if it isn't I don't need destroying. Where would you add a call to destroy()? \$\endgroup\$ – bitmask Nov 24 '16 at 18:28
  • \$\begingroup\$ Further note: If I have T = vector<U>, then destroying and recreating it, guarantees a malloc, however, that malloc can be saved, when I give the vector a chance to reuse the already allocated storage by simply using the assignment operator. \$\endgroup\$ – bitmask Nov 24 '16 at 18:30
  • \$\begingroup\$ @bitmask, you're right. Ive got confused by its name. Not a good one \$\endgroup\$ – Incomputable Nov 24 '16 at 18:35
  • \$\begingroup\$ I considered create_assign but decided it was a quite mouth full :) \$\endgroup\$ – bitmask Nov 24 '16 at 18:41

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.