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;
}
}
};
}
1 Answer 1
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.
-
\$\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– bitmask2016年11月24日 18:28:02 +00:00Commented Nov 24, 2016 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– bitmask2016年11月24日 18:30:07 +00:00Commented Nov 24, 2016 at 18:30
-
\$\begingroup\$ @bitmask, you're right. Ive got confused by its name. Not a good one \$\endgroup\$Incomputable– Incomputable2016年11月24日 18:35:44 +00:00Commented Nov 24, 2016 at 18:35
-
\$\begingroup\$ I considered
create_assign
but decided it was a quite mouth full :) \$\endgroup\$bitmask– bitmask2016年11月24日 18:41:21 +00:00Commented Nov 24, 2016 at 18:41
std::optional
. I would advocate that optional is much better name rather than Maybe. Maybe sounds rather humorous. \$\endgroup\$