Created
June 22, 2024 15:12
-
-
Save Nekrolm/471e8d1c0810b19fe3db70e389e588ee to your computer and use it in GitHub Desktop.
C++ optional with destructive move
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://godbolt.org/z/cfE7sh3jT | |
#include <type_traits> | |
#include <utility> | |
#include <new> | |
#include <stdexcept> | |
#include <functional> | |
#include <print> | |
#include <string> | |
#include <vector> | |
template <class T> | |
struct niche { | |
// using storage_type = ???; | |
// constexpr static storage_type niche_value = ???; | |
}; | |
template <class T> | |
struct niche<T&> { | |
using storage_type = T*; | |
constexpr static storage_type niche_value = nullptr; | |
static storage_type make(T& value) { | |
return &value; | |
} | |
static T& get(storage_type s) { | |
return *s; | |
} | |
}; | |
template <class T> | |
struct niche<const T&> { | |
using storage_type = const T*; | |
constexpr static storage_type niche_value = nullptr; | |
static storage_type make(const T& value) { | |
return &value; | |
} | |
static const T& get(storage_type s) { | |
return *s; | |
} | |
}; | |
template <class T> | |
concept has_niche = requires(T val, niche<T>::storage_type& storage) { | |
{ auto(niche<T>::niche_value) } -> std::same_as<typename niche<T>::storage_type>; | |
{ niche<T>::make(val) } -> std::same_as<typename niche<T>::storage_type>; | |
{ niche<T>::niche_value == niche<T>::make(val) }; | |
{ niche<T>::get(storage) } -> std::same_as<T>; | |
}; | |
struct none_t {}; | |
static constexpr none_t None; | |
template <has_niche T> | |
struct NicheStorage { | |
explicit NicheStorage(none_t) : storage { niche_t::niche_value } {} | |
explicit NicheStorage(T v): storage { niche_t::make(v) } {} | |
NicheStorage take() { | |
return NicheStorage { std::exchange(this->storage, niche_t::niche_value) }; | |
} | |
NicheStorage insert(T&& value) { | |
NicheStorage v { std::move(value) }; | |
std::swap(v.storage, this->storage); | |
return v; | |
} | |
bool is_none() const { | |
return this->storage == niche_t::niche_value; | |
} | |
T& as_inner() & { | |
return niche_t::get(this->storage); | |
} | |
const T& as_inner() const &{ | |
return niche_t::get(this->storage); | |
} | |
private: | |
using niche_t = niche<T>; | |
using storage_t = niche_t::storage_type; | |
storage_t storage; | |
}; | |
/// This storage assumes destructive move semantics: | |
/// Even in C++, any non self-referential object can be moved | |
/// via memcopy and disabling destructors. We only loose potential side effects of move constructor | |
/// like logging. But you should never rely on such effects of move-constructors | |
template <class T> | |
struct MaybeUninitStorage { | |
~MaybeUninitStorage() requires (!std::is_trivially_destructible_v<T>) { | |
if (this->initialized) { | |
this->as_inner().~T(); | |
} | |
this->initialized = false; | |
} | |
~MaybeUninitStorage() = default; | |
explicit MaybeUninitStorage(none_t) {}; | |
template <class... Args> | |
MaybeUninitStorage(Args&&... args) { | |
new(this->s.storage) T (std::forward<Args>(args)...); | |
this->initialized = true; | |
} | |
MaybeUninitStorage(MaybeUninitStorage&& tmp) { | |
this->s = tmp.s; | |
this->initialized = std::exchange(tmp.initialized, false); | |
} | |
MaybeUninitStorage(const MaybeUninitStorage& other) { | |
if (other.initialized) { | |
new (this->s.storage) T (other.as_inner()); | |
this->initialized = true; | |
} | |
} | |
void operator = (MaybeUninitStorage&& tmp) { | |
MaybeUninitStorage local_tmp { std::move(tmp) }; | |
this->swap(local_tmp); | |
} | |
void operator = (const MaybeUninitStorage& other) { | |
MaybeUninitStorage local_tmp { other }; | |
this->swap(local_tmp); | |
} | |
bool is_none() const { | |
return !this->initialized; | |
} | |
MaybeUninitStorage take() { | |
return MaybeUninitStorage { std::move(*this) }; | |
} | |
MaybeUninitStorage insert(T&& value) { | |
MaybeUninitStorage temp { std::move(value) }; | |
this->swap(temp); | |
return temp; | |
} | |
void swap(MaybeUninitStorage& other) { | |
std::swap(other.s, this->s); | |
std::swap(other.initialized, this->initialized); | |
} | |
T& as_inner() & { | |
return *std::launder(reinterpret_cast<T*>(this->s.storage)); | |
} | |
const T& as_inner() const &{ | |
return *std::launder(reinterpret_cast<const T*>(this->s.storage)); | |
} | |
private: | |
struct RawStorage { | |
alignas(T) unsigned char storage[sizeof(T)] = {}; | |
} s; | |
bool initialized = false; | |
}; | |
template<class T> | |
struct type { | |
using t = T; | |
}; | |
template <class T> | |
auto storage_type() { | |
if constexpr (has_niche<T>) { | |
return type<NicheStorage<T>>{}; | |
} else { | |
return type<MaybeUninitStorage<T>>{}; | |
} | |
} | |
template <class T> | |
struct Option : private decltype(storage_type<T>())::t { | |
private: | |
using storage = decltype(storage_type<T>())::t; | |
public: | |
using storage::storage; | |
using storage::is_none; | |
bool is_some() const { | |
return !this->is_none(); | |
} | |
Option(Option&& tmp) : storage(static_cast<storage&&>(tmp)) {} | |
Option(const Option& tmp) : storage(static_cast<const storage&>(tmp)) {} | |
void operator = (Option&& tmp) { | |
*this = static_cast<storage&&>(tmp); | |
} | |
void operator = (const Option& other) { | |
*this = static_cast<const storage&>(other); | |
} | |
T unwrap() && { | |
if (this->is_some()) { | |
T ret = std::move(this->as_inner()); | |
return ret; | |
} else { | |
throw std::runtime_error("unwrapping none optional"); | |
} | |
} | |
auto map(this auto&& self, auto&& f) -> Option<std::invoke_result_t<decltype(f), decltype(self.as_inner())>> { | |
using result_t = std::invoke_result_t<decltype(f), decltype(self.as_inner())>; | |
if (self.is_some()) { | |
return Option<result_t> { std::invoke(std::forward<decltype(f)>(f), self.as_inner()) }; | |
} else { | |
return Option<result_t> { None }; | |
} | |
} | |
Option<const T&> as_ref() const& { | |
return this->map([](const T& x) -> const T& { return x; }); | |
} | |
Option<T&> as_mut() & { | |
return this->map([](T& x) -> T& { return x; }); | |
} | |
auto and_then(this auto&& self, auto&& f) -> std::invoke_result_t<decltype(f), decltype(self.as_inner())> { | |
if (self.is_some()) { | |
return std::invoke(std::forward<decltype(f)>(f), self.as_inner()); | |
} else { | |
return Option { None }; | |
} | |
} | |
Option<T> take() & { | |
return Option { (this->*&storage::take)() }; | |
} | |
Option<T> insert(T&& x) { | |
if constexpr(std::is_rvalue_reference_v<decltype(x)>) { | |
return Option { (this->*&storage::insert)(std::move(x)) }; | |
} else { | |
return Option { (this->*&storage::insert)(x) }; | |
} | |
} | |
private: | |
explicit Option(storage&& s) : storage { std::move(s)} {} | |
}; | |
template <class T> | |
auto Some(T&& x) { | |
if constexpr (std::is_rvalue_reference_v<decltype(x)>) { | |
return Option<std::decay_t<T>>{ std::move(x) }; | |
} else { | |
return Option<decltype(x)> { x }; | |
} | |
} | |
int main() { | |
Option<int> opt = Some(15); | |
static_assert(sizeof(opt) == 8); | |
static_assert(has_niche<int&>); | |
auto opt_ref = opt.as_mut(); | |
static_assert(sizeof(opt_ref) == 8); | |
opt_ref.map([](int& val) { val +=1; return val; }); | |
auto opt_str = Some(std::string("123456789AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")); | |
opt_str.take(); // Ok, string is at the heap | |
// boom! string is on the stack and std::string SSO is self-referential | |
// auto old = opt_str.insert("12345667"); | |
auto opt_vec = Some(std::vector{1,2,3,4,5}); | |
opt_vec.take(); | |
auto old = opt_vec.insert({2,3,4,5}); | |
std::println("old vec is none: {}", old.is_none()); | |
opt_vec.map([](auto&& v) { | |
std::println("v size={}", v.size()); | |
return true; // special case for void is not implemented... | |
}); | |
return std::move(opt).unwrap(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment