Skip to content

Instantly share code, notes, and snippets.

@Nekrolm
Created June 22, 2024 15:12
Show Gist options
  • Save Nekrolm/471e8d1c0810b19fe3db70e389e588ee to your computer and use it in GitHub Desktop.
Save Nekrolm/471e8d1c0810b19fe3db70e389e588ee to your computer and use it in GitHub Desktop.
C++ optional with destructive move
// 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