Files
asl/asl/types/option.hpp

460 lines
12 KiB
C++

// Copyright 2025 Steven Le Rouzic
//
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include "asl/base/assert.hpp"
#include "asl/base/meta.hpp"
#include "asl/base/functional.hpp"
#include "asl/base/annotations.hpp"
#include "asl/types/maybe_uninit.hpp"
#include "asl/hashing/hash.hpp"
namespace asl
{
struct nullopt_t {};
static constexpr nullopt_t nullopt{};
// @Todo(option) Reference
// @Todo(option) Function
// @Todo(option) Arrays
template<is_object T> class option;
namespace option_internal
{
template<typename T, typename U>
concept not_constructible_from_option =
!constructible_from<T, option<U>&> &&
!constructible_from<T, const option<U>&> &&
!constructible_from<T, option<U>&&> &&
!constructible_from<T, const option<U>&&>;
template<typename T, typename U>
concept not_assignable_from_option =
!assignable_from<T&, option<U>&> &&
!assignable_from<T&, const option<U>&> &&
!assignable_from<T&, option<U>&&> &&
!assignable_from<T&, const option<U>&&>;
template<typename T, typename U>
concept not_constructible_assignable_from_option =
not_constructible_from_option<T, U> &&
not_assignable_from_option<T, U>;
} // namespace option_internal
template<typename T>
concept is_option = requires
{
typename T::type;
requires same_as<un_cvref_t<T>, option<typename T::type>>;
};
template<is_object T>
class option
{
static constexpr bool kHasNiche = has_niche<T>;
using HasValueMarker = select_t<kHasNiche, empty, bool>;
maybe_uninit<T> m_payload{};
ASL_NO_UNIQUE_ADDRESS HasValueMarker m_has_value{};
template<is_object U>
friend class option;
template<typename... Args>
constexpr void construct(Args&&... args)
{
ASL_ASSERT(!has_value());
if constexpr (!kHasNiche)
{
m_payload.construct_unsafe(std::forward<Args>(args)...);
m_has_value = true;
}
else
{
if constexpr (move_assignable<T>)
{
m_payload.assign_unsafe(T{std::forward<Args>(args)...});
}
else
{
m_payload.destroy_unsafe();
m_payload.construct_unsafe(std::forward<Args>(args)...);
}
}
}
template<typename U>
constexpr void assign(U&& arg)
{
ASL_ASSERT(has_value());
m_payload.assign_unsafe(std::forward<U>(arg));
}
public:
using type = T;
constexpr option() : option{nullopt} {}
// NOLINTNEXTLINE(*explicit*)
constexpr option(nullopt_t) requires (!kHasNiche) {}
// NOLINTNEXTLINE(*explicit*)
constexpr option(nullopt_t) requires kHasNiche : m_payload{in_place, niche_t{}} {}
template<typename U = T>
constexpr explicit (!convertible_to<U&&, T>)
option(U&& value)
requires (
kHasNiche &&
constructible_from<T, U&&> &&
!same_as<un_cvref_t<U>, option>
)
: m_payload{in_place, std::forward<U>(value)}
{}
template<typename U = T>
constexpr explicit (!convertible_to<U&&, T>)
option(U&& value)
requires (
!kHasNiche &&
constructible_from<T, U&&> &&
!is_option<U>
)
: m_payload{in_place, std::forward<U>(value)}
, m_has_value{true}
{}
constexpr option(const option& other) requires trivially_copy_constructible<T> = default;
constexpr option(const option& other) requires (!copy_constructible<T>) = delete;
constexpr option(const option& other)
requires copy_constructible<T> && (!trivially_copy_constructible<T>)
: option{nullopt}
{
if (other.has_value())
{
construct(other.m_payload.as_init_unsafe());
}
}
constexpr option(option&& other) requires trivially_move_constructible<T> = default;
constexpr option(option&& other) requires (!move_constructible<T>) = delete;
constexpr option(option&& other)
requires move_constructible<T> && (!trivially_move_constructible<T>)
: option{nullopt}
{
if (other.has_value())
{
construct(std::move(other.m_payload.as_init_unsafe()));
}
}
template<typename U>
constexpr explicit (!convertible_to<const U&, T>)
option(const option<U>& other)
requires (
constructible_from<T, const U&> &&
option_internal::not_constructible_from_option<T, U>
)
: option{nullopt}
{
if (other.has_value())
{
construct(other.m_payload.as_init_unsafe());
}
}
template<typename U>
constexpr explicit (!convertible_to<U&&, T>)
option(option<U>&& other)
requires (
constructible_from<T, U&&> &&
option_internal::not_constructible_from_option<T, U>
)
: option{nullopt}
{
if (other.has_value())
{
construct(std::move(other).m_payload.as_init_unsafe());
}
}
constexpr option& operator=(nullopt_t) &
{
reset();
return *this;
}
template<typename U = T>
constexpr option& operator=(U&& value) &
requires (
assignable_from<T&, U&&> &&
constructible_from<T, U&&> &&
!is_option<U>
)
{
if (has_value())
{
assign(std::forward<U>(value));
}
else
{
construct(std::forward<U>(value));
}
return *this;
}
constexpr option& operator=(const option& other) &
requires (!copy_assignable<T>) = delete;
constexpr option& operator=(const option& other) &
requires trivially_copy_assignable<T> = default;
constexpr option& operator=(const option& other) &
requires copy_assignable<T> && (!trivially_copy_constructible<T>)
{
if (&other == this) { return *this; }
if (other.has_value())
{
if (has_value())
{
assign(other.m_payload.as_init_unsafe());
}
else
{
construct(other.m_payload.as_init_unsafe());
}
}
else if (has_value())
{
reset();
}
return *this;
}
constexpr option& operator=(option&& other) &
requires (!move_assignable<T>) = delete;
constexpr option& operator=(option&& other) &
requires trivially_move_assignable<T> = default;
constexpr option& operator=(option&& other) &
requires move_assignable<T> && (!trivially_move_constructible<T>)
{
if (&other == this) { return *this; }
if (other.has_value())
{
if (has_value())
{
assign(std::move(other.m_payload.as_init_unsafe()));
}
else
{
construct(std::move(other.m_payload.as_init_unsafe()));
}
}
else if (has_value())
{
reset();
}
return *this;
}
template<typename U = T>
constexpr option& operator=(const option<U>& other) &
requires (
constructible_from<T, const U&> &&
assignable_from<T&, const U&> &&
option_internal::not_constructible_assignable_from_option<T, U>
)
{
if (other.has_value())
{
if (has_value())
{
assign(other.m_payload.as_init_unsafe());
}
else
{
construct(other.m_payload.as_init_unsafe());
}
}
else if (has_value())
{
reset();
}
return *this;
}
template<typename U = T>
constexpr option& operator=(option<U>&& other) &
requires (
constructible_from<T, U&&> &&
assignable_from<T&, U&&> &&
option_internal::not_constructible_assignable_from_option<T, U>
)
{
if (other.has_value())
{
if (has_value())
{
assign(std::move(other).m_payload.as_init_unsafe());
}
else
{
construct(std::move(other).m_payload.as_init_unsafe());
}
}
else if (has_value())
{
reset();
}
return *this;
}
constexpr ~option() requires trivially_destructible<T> = default;
constexpr ~option() requires (!trivially_destructible<T>)
{
reset();
}
constexpr void reset()
{
if (!has_value()) { return; }
if constexpr (kHasNiche)
{
if constexpr (move_assignable<T>)
{
m_payload.assign_unsafe(std::move(T{niche_t{}}));
}
else
{
m_payload.destroy_unsafe();
m_payload.construct_unsafe(niche_t{});
}
}
else
{
m_has_value = false;
m_payload.destroy_unsafe();
}
}
[[nodiscard]] constexpr bool has_value() const
{
if constexpr (kHasNiche)
{
return m_payload.as_init_unsafe() != niche_t{};
}
else
{
return m_has_value;
}
}
constexpr auto&& value(this auto&& self)
{
ASL_ASSERT_RELEASE(self.has_value());
return std::forward<decltype(self)>(self).m_payload.as_init_unsafe();
}
template<typename U>
constexpr T value_or(U&& other_value) const&
requires copy_constructible<T> && convertible_to<U&&, T>
{
return has_value() ? value() : static_cast<T>(std::forward<U>(other_value));
}
template<typename U>
constexpr T value_or(U&& other_value) &&
requires move_constructible<T> && convertible_to<U&&, T>
{
return has_value() ? std::move(value()) : static_cast<T>(std::forward<U>(other_value));
}
constexpr T& emplace(auto&&... args) &
requires constructible_from<T, decltype(args)...>
{
if (has_value()) { reset(); }
construct(std::forward<decltype(args)>(args)...);
return value();
}
template<typename F>
constexpr auto and_then(this auto&& self, F&& f)
{
using Result = invoke_result_t<F, copy_cref_t<decltype(self), T>>;
static_assert(is_option<Result>);
if (self.has_value())
{
return invoke(std::forward<F>(f), std::forward<decltype(self)>(self).value());
}
return Result{ asl::nullopt };
}
template<typename F>
constexpr auto transform(this auto&& self, F&& f)
{
using Result = invoke_result_t<F, copy_cref_t<decltype(self), T>>;
if (self.has_value())
{
return option<un_cvref_t<Result>>{
invoke(std::forward<F>(f), std::forward<decltype(self)>(self).value())
};
}
return option<un_cvref_t<Result>>{ asl::nullopt };
}
template<typename F>
constexpr option or_else(F&& f) const&
requires same_as<un_cvref_t<invoke_result_t<F>>, option>
{
return has_value() ? *this : invoke(std::forward<F>(f));
}
template<typename F>
constexpr option or_else(F&& f) &&
requires same_as<un_cvref_t<invoke_result_t<F>>, option>
{
return has_value() ? std::move(*this) : invoke(std::forward<F>(f));
}
template<typename H>
requires (!uniquely_represented<option>) && hashable<T>
friend H AslHashValue(H h, const option& opt)
{
if (!opt.has_value())
{
return H::combine(std::move(h), 0);
}
return H::combine(std::move(h), 1, opt.value());
}
};
template<typename T>
requires has_niche<T> && uniquely_represented<T>
struct is_uniquely_represented<option<T>> : true_type {};
template<typename T>
option(T) -> option<T>;
} // namespace asl