From e78c765f452579e67899b7b32f77dab569386e5b Mon Sep 17 00:00:00 2001 From: Avril Date: Tue, 16 May 2023 10:43:47 +0100 Subject: [PATCH] Added better null checks and assumptions. removed `is_boxed()` meta-polymorphism. added Box children (non-virtual) to perform unsafe operations that may invalidate Box invariant. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fortune for libexopt's current commit: Future blessing − 末吉 --- include/boxed.h | 194 ++++++++++++++++++++------------------------- include/exopt.h | 21 ++++- include/optional.h | 4 +- include/pointer.h | 34 ++++++++ include/util.hh | 100 +++++++++++++++++++++-- 5 files changed, 232 insertions(+), 121 deletions(-) diff --git a/include/boxed.h b/include/boxed.h index 3f32cee..97b9301 100644 --- a/include/boxed.h +++ b/include/boxed.h @@ -10,14 +10,13 @@ #include "exopt.h" namespace exopt::types { namespace boxed { - template struct [[gnu::visibility("internal")]] boxable_value_type { using type = std::conditional_t ,std::reference_wrapper > ,T>; }; - namespace { + namespace { template constexpr std::unique_ptr uniq_clone(std::unique_ptr const& c) noexcept(std::is_nothrow_copy_constructible_v) requires(std::is_copy_constructible_v) @@ -31,7 +30,7 @@ namespace exopt::types { namespace boxed { requires(std::is_copy_constructible_v) { if(__builtin_expect(!bool(c), false)) #ifdef DEBUG - throw ptr::null_exception{}; + throw ptr::NullDerefException{}; #else __builtin_unreachable(); #endif @@ -63,17 +62,16 @@ namespace exopt::types { namespace boxed { { static_cast &&>(from) } -> std::convertible_to; }; - struct ErasedTypeDeleter { + /*struct ErasedTypeDeleter { constexpr virtual ~ErasedTypeDeleter() = default; constexpr ErasedTypeDeleter() noexcept = default; constexpr virtual void apply_delete() = 0; //TODO: See below... - }; + };*/ template T> constexpr auto uniq_erase_type(std::unique_ptr&& from) noexcept { - //TODO: Overload case for `unique_ptr` for `deleter` to apply (and contain) `D()` on back_cast()ed pointer T* instead of assuming default `delete`. struct deleter final /*: public ErasedTypeDeleter XXX <- Is this useful, for unerasing the type, maybe?*/ { typedef T type; @@ -94,18 +92,26 @@ namespace exopt::types { namespace boxed { delete static_cast(p); } constexpr T* back_cast(void *p) noexcept { return static_cast(p); } - /*constexpr deleter() noexcept = default; - constexpr ~deleter() noexcept = default;*/ + constexpr deleter() noexcept = default; + constexpr deleter(deleter const&) noexcept = default; + constexpr deleter(deleter &&) noexcept = default; + constexpr deleter& operator=(deleter const&) noexcept = default; + constexpr deleter& operator=(deleter &&) noexcept = default; + + constexpr ~deleter() noexcept = default; }; return std::unique_ptr { - //XXX: Cannot retain type information of most-derived class from this: \ + // Cannot retain type information of most-derived class from this: \ dynamic_cast static_cast (std::move(from).release()) }; } - template T, typename D> requires(requires{ typename std::unique_ptr; }) + template T, typename D> requires(requires{ + typename std::unique_ptr; + requires(!std::is_same_v::deleter_type, std::unique_ptr::deleter_type); + }) constexpr auto uniq_erase_type(std::unique_ptr&& from) noexcept { class deleter final { D m_del; @@ -127,6 +133,7 @@ namespace exopt::types { namespace boxed { constexpr deleter& operator=(const deleter&) noexcept(std::is_nothrow_copy_assignable_v) = default; constexpr deleter& operator=(deleter&&) noexcept(std::is_nothrow_move_assignable_v) = default; + constexpr ~deleter() noexcept(std::is_nothrow_destructible_v) requires(std::is_trivially_destructible_v) = default; constexpr ~deleter() noexcept(std::is_nothrow_destructible_v) {} }; if constexpr(std::is_default_constructible_v) @@ -166,72 +173,8 @@ namespace exopt::types { namespace boxed { else return static_uniq_cast(std::move(from)); } -#if 0 -//TODO: Cannot be implemented with this method - constexpr bool is_boxed_value(auto const& value) noexcept { - return bool(dynamic_cast(std::addressof(value))); - } - template - constexpr bool is_boxed_type() noexcept { - using type = std::remove_reference_t; - constexpr type const* VALUE = nullptr; - return bool(dynamic_cast(VALUE)); - } - -// We need either: Tagged pointers (XOR, bit-mangled, manual heap, or GOT (external allocation) kind), \ - or (preferred): layout of Box to be `{ aligned(T, box_type_tag) } unique*` \ - box_type_tag should be: One pointer width, starting at offset *T+1 (aligned to T within internal tuple struct), provides a way to access RTTI for `*T`, a pointer to static (non-allocated) type information or dynamic_cast used lambda would be ideal. -#endif - /// Base class which is used to store the layout of Box's allocated memory. Intended for use to determine if a certain `T`'s `this` pointer is inside a `Box` or not. - class dynamic_boxed_layout_base{ - using Self = dynamic_boxed_layout_base; - public: - constexpr dynamic_boxed_layout_base() noexcept = default; - constexpr ~dynamic_boxed_layout_base() = default; - - typedef void* (dynamic_cast_t)(Self*); - typedef const void* (dynamic_const_cast_t)(Self const*); - - constexpr virtual size_t value_byte_offset() const noexcept =0; - - [[gnu::returns_nonnull]] - constexpr void* extract_untyped_pointer() noexcept { - return reinterpret_cast( - reinterpret_cast(unsigned char*) + value_byte_offset() - ); - } - [[gnu::returns_nonnull]] - constexpr const void* extract_untyped_pointer() const noexcept { - return reinterpret_cast( - reinterpret_cast(const unsigned char*) + value_byte_offset() - ); - } - - constexpr virtual void* invoke_casting_shim(dynamic_cast_t const*) const noexcept =0; - - template - constexpr bool is_typeof() const noexcept { return invoke_casting_shim([] (Self* self) { return dynamic_cast(self) - - template - [[gnu::returns_nonnull]] - inline T* extract_pointer_unsafe() noexcept { - return reinterpret_cast(extract_untyped_pointer()); - } - template - [[gnu::returns_nonnull]] - inline const T* extract_pointer_unsafe() const noexcept { - return reinterpret_cast(extract_untyped_pointer()); - } - - template - [[gnu::returns_nonnull]] - inline T* try_extract_pointer() noexcept { - return is_typeof() ? extract_pointer_unsafe() : nullptr; - } - }; - template - struct Box final { + struct alignas(std::unique_ptr) Box { typedef boxable_value_type::type type; template U, bool RuntimeCheck=false> @@ -277,15 +220,24 @@ namespace exopt::types { namespace boxed { } [[gnu::nonnull(1)]] - constexpr static Box from_raw_ptr(T* ptr) noexcept { + constexpr static Box from_raw_ptr(T* ptr) noexcept(!_EO(DEBUG)) { +#if DEBUG + ptr::throw_if_null(ptr); +#endif return Box{ std::unique_ptr{ ptr } }; } - constexpr static Box from_raw_ptr(std::unique_ptr&& uniq) + + constexpr static Box from_raw_ptr(std::unique_ptr&& uniq) { + if(__builtin_expect(!bool(uniq), false)) ptr::throw_null_exception(); + _EO_ASSUME(uniq.get() != nullptr); + return Box { std::move(uniq) }; + } + + constexpr static Box from_raw_ptr_unchecked(std::unique_ptr&& uniq) noexcept(!_EO(DEBUG)) { #if DEBUG - { if(__builtin_expect(!uniq, false)) throw ptr::NullException{}; + if(__builtin_expect(!uniq, false)) ptr::throw_null_exception(); #else - noexcept { _EO_ASSUME(uniq.get() != nullptr); #endif return Box{ std::move(uniq) }; @@ -300,49 +252,63 @@ namespace exopt::types { namespace boxed { //TODO: Accessors, `(explicit?) operator std::unique_ptr const&[&]`?, etc. constexpr ~Box() = default; - private: - constexpr explicit Box(std::unique_ptr&& ptr) - : m_ptr(std::move(ptr)) { - _EO_ASSUME(m_ptr.get()); - } + protected: + // For types that may need to invalidate the Box guarantee without exposing it in API, they can inherit (non-virtual). -// TODO: Identifying if a value `this` pointer is boxed - struct alignas(type) inner_layout_t final : public dynamic_boxed_layout_base { - using boxed_value_t = type; + // For unsafe operations, wether or not to apply checks. + enum unsafe_t { + UNSAFE, + }; + enum maybe_unsafe_t { + CHECKED, + DEBUG_CHECKED, + }; - constexpr inner_layout_t(type&& v) noexcept(std::is_nothrow_move_constructible_v) - : value(std::move(v)) {} - //TODO: All the ctors, assigns, and operators that can make this newtype wrapper more conveniently useable as a deref-target for accessing `value`. - - constexpr /*virtual*/ ~inner_layout_t() = default; + constexpr Box(util::exact_type auto u, std::unique_ptr&& ptr) noexcept + : m_ptr(std::move(ptr)) { (void)u; } + + constexpr static Box from_raw_ptr(unsafe_t u, std::unique_ptr&& uniq) noexcept { + return Box{u, std::move(uniq)}; + } - [[gnu::const]] - constexpr size_t value_byte_offset() const noexcept override { return offsetof(inner_layout_t, value); } + constexpr std::unique_ptr& as_unique(unsafe_t) & noexcept { return m_ptr; } + constexpr std::unique_ptr const & as_unique(unsafe_t) const& noexcept { return m_ptr; } - type value; - }; - - struct inner_unique_ptr_t final { - std::unique_ptr - }; + constexpr std::unique_ptr&& as_unique(unsafe_t) && noexcept { return std::move(m_ptr); } + constexpr std::unique_ptr const&& as_unique(unsafe_t) const&& noexcept { return std::move(m_ptr); } - inner_unique_ptr_t m_ptr; // Unique m_ptr; - }; + constexpr T* as_unsafe_ptr(unsafe_t) const noexcept { return m_ptr.get(); } + constexpr std::unique_ptr swap_unsafe_ptr(unsafe_t u, T* raw) noexcept + { return (void)u; std::exchange(std::move(m_ptr), std::unique_ptr{ raw }); } + constexpr void set_unsafe_ptr(unsafe_t 8, T* raw) noexcept { + (void)u; + m_ptr.reset(raw); + } + constexpr decltype(auto) set_unsafe(unsafe_t u, std::unique_ptr&& ptr) noexcept + { (void)u; return std::exchange(std::move(m_ptr), std::move(ptr)); } + constexpr auto& swap_unsafe(unsafe_t, std::unique_ptr& ptr) noexcept { + using std::swap; + swap(m_ptr, ptr); + return m_ptr; + } -#define _EO_ADD_RV std::add_rvalue_reference_v + constexpr T* release_unsafe(unsafe_t) noexcept { return m_ptr.release(); } + private: + constexpr explicit Box(std::unique_ptr&& ptr) noexcept + : m_ptr(std::move(ptr)) { + _EO_ASSUME(m_ptr.get()); + } - template - constexpr inline bool is_boxable_v = binary_convertible<_EO_ADD_RV, _EO_ADD_RV > and requires(T&& value) { - typename Box; - typename Box::type; - { std::make_unique(std::move(value)) } -> std::same_as>; + std::unique_ptr m_ptr; // Unique m_ptr; }; + static_assert(util::shares_layout, _Complex double*>, "Invalid Box memory layout: More than just T*"); + static_assert(util::shares_layout, std::unique_ptr<_Complex double>>, "Invalid Box memory layout: More than just std::unique_ptr"); + template concept is_boxable = is_boxable_v; template constexpr inline bool is_nothrow_boxable_v = is_boxable_v && std::is_nothrow_constructible_v, T>; -#undef _EO_ADD_RV template requires(requires(T&& o) { static_cast(o); }) constexpr Box static_box_cast(Box&& b) noexcept { return Box::from_raw_ptr(static_cast(b.release())); } @@ -356,7 +322,12 @@ namespace exopt::types { namespace boxed { delete rel; throw; } - } //TODO: Overload for `const&` that does the type check *before* the copy allocation. + } + + template requires(requires(T const& o) { static_cast(o); }) + constexpr Box dynamic_box_cast(Box const& b) { + return Box::from_raw_ptr(std::addressof(dynamic_cast(*b))); + } #if 0 template requires(std::derived_from) @@ -399,6 +370,8 @@ namespace exopt::types { namespace boxed { using boxed::Box; } +//TODO: See phone notes on correctly implementing the nullopt for Box +#if 0 namespace exopt::types { namespace optional [[gnu::visibility("internal")]] { template struct null_optimise > { constexpr static inline bool value = true; @@ -413,3 +386,4 @@ namespace exopt::types { namespace optional [[gnu::visibility("internal")]] { } +#endif diff --git a/include/exopt.h b/include/exopt.h index 7ef0b7a..7041c70 100644 --- a/include/exopt.h +++ b/include/exopt.h @@ -22,17 +22,36 @@ extern "C" { #define _exopt__internal __attribute__((visibility("internal"))) #define _exopt__readonly __attribute__((__pure__)) #define _exopt__pure __attribute__((__const__)) +#ifdef DEBUG +# define _exopt__DEBUG 1 +#else +# define _exopt__DEBUG 0 +#endif +#ifdef RELEASE +# define _exopt__RELEASE 1 +#else +# define _exopt__RELEASE 0 +#endif #ifdef _EO__OPT_OLD_ASSUME # define _EO_ASSUME(X) ({ if(! (X)) __builtin_unreachable(); }) #endif +#define _EO_FIXED_INT(B) _BitInt(B) + +#define _EO_GNU_ATTR(...) __attribute__((__VA_ARGS__)) + #if _EO(cpp) +# define _EO_ATTR(...) [[__VA_ARGS__]] +# define _EO_SHARES_LAYOUT(T, U) (sizeof(T) == sizeof(U) && alignof(T) == alignof(U)) # ifndef _EO_ASSUME # define _EO_ASSUME(X) [[gnu::assume(X)]] # endif # define _exopt__restrict __restrict__ -#else +#else /* C */ +# define _EO_ATTR(...) __attribute__((__VA_ARGS__)) +# define _EO_SHARES_LAYOUT(T, U) (sizeof(T) == sizeof(U) && alignof(T) == alignof(U)) +# define _EO_SHARES_LAYOUT(T, U) (sizeof(T) == sizeof(U) && _Alignof(T) == _Alignof(U)) # ifndef _EO_ASSUME # define _EO_ASSUME(X) __attribute__((assume(X))) # endif diff --git a/include/optional.h b/include/optional.h index 09da245..c5b2bb8 100644 --- a/include/optional.h +++ b/include/optional.h @@ -29,9 +29,7 @@ namespace exopt::types { namespace optional [[gnu::visibility("internal")]] { constexpr static decltype(auto) convert_from_held(std::add_rvalue_reference_t t) noexcept { return std::ref(*t); } constexpr static inline auto sentinel = nullptr; - }; - - + }; template struct null_optimise > { constexpr static inline bool value = true; diff --git a/include/pointer.h b/include/pointer.h index 5498f8e..65d7908 100644 --- a/include/pointer.h +++ b/include/pointer.h @@ -104,6 +104,33 @@ namespace exopt::ptr { return std::forward(ptr); } + template requires(std::is_constructible_v) + constexpr decltype(auto) throw_if_null(PointerEqCmp auto const& ptr, Args&&... error) { + auto _throw = [&] [[noreturn, gnu::noinline, gnu::cold]] () { + throw E{std::forward(error)...}; + }; + if(ptr == nullptr) _throw(); + return std::forward(ptr); + } + + + [[gnu::always_inline]] + constexpr decltype(auto) throw_if_null(PointerEqCmp auto const& ptr) { + if(__builtin_expect(ptr == nullptr, false)) throw_null_exception(); + return std::forward(ptr); + } + + // Throws NullDerefException for first type of `ptr` that is null (if any are). + // If: `All == true`: Instead will always throw `NullDerefException (all types) if any are, and will not assume every pointer is likely to be non-null. + template + [[gnu::always_inline]] + constexpr void throw_if_null(PointerEqCmp auto const&... ptr) requires(sizeof...(decltype(ptr)) > 1) { + if constexpr(All) { + using E = NullDerefException; + ((void)throw_if_null(std::forward(ptr)),...); + } else ((void)throw_if_null(std::forward(ptr)),...); + } + template requires(std::is_constructible_v) constexpr decltype(auto) deref_or_throw(PointerDeref auto&& ptr, Args&&... error) { @@ -190,6 +217,13 @@ namespace exopt::ptr { consteval NullException/*&&*/ null_exception() noexcept { return {}; } + template + [[noreturn, gnu::noinline, gnu::cold]] + constexpr void throw_null_exception() { throw null_exception(); } + + [[noreturn, gnu::noinline, gnu::cold]] + constexpr void throw_null_exception() { throw null_exception(); } + template requires(!std::is_reference_v) struct NonNull { diff --git a/include/util.hh b/include/util.hh index 4d91ed8..d35555c 100644 --- a/include/util.hh +++ b/include/util.hh @@ -68,6 +68,12 @@ namespace exopt { namespace util [[gnu::visibility("internal")]] { static_assert(_EO_CONSTANT_VALUE(std::string_view{"hello world"}) == std::string_view{"hello world"}, "Bad CONSTANT_VALUE()"); + template + concept shares_layout = sizeof(T) == sizeof(U) && alignof(T) == alignof(U); + + template + concept exact_type = std::is_same_v; + template, typename _I = std::make_index_sequence> constexpr auto array_literal(const T (&a)[N]) noexcept -> Array @@ -178,17 +184,90 @@ namespace exopt { namespace util [[gnu::visibility("internal")]] { static_assert(concat_str_v< std::string_view{"hello"}, std::string_view{" "}, std::string_view{"view"} > == std::string_view{"hello view"}, "concat_str_v<>: Concatenated string_view failed"); + template P, typename... Values> + struct _EO(internal) is_all_match { + constexpr static inline auto value = (P && true && ...); + + consteval operator auto() const noexcept { return value; } + }; + + template P, typename... Values> + struct _EO(internal) is_any_match { + constexpr static inline auto value = (P || false || ...); + + consteval operator auto() const noexcept { return value; } + }; + + //XXX: Idk if template P, typename... Values> requires(requires{ typename is_all_match; }) + constexpr inline auto is_all_match_v = is_all_match::value; + + template P, typename... Values> requires(requires{ typename is_any_match; }) + constexpr inline auto is_any_match_v = is_any_match::value; + + template + struct _EO(internal) is_all { + constexpr static inline auto value = (Values && true && ...); + }; + + template + struct _EO(internal) is_any { + constexpr static inline auto value = (Values || false || ...); + }; + + template + constexpr inline auto is_all_v = is_all::value; + template + constexpr inline auto is_any_v = is_any::value; + + template + constexpr inline bool is_empty_v = sizeof...(Args) == 0; + + template // auto and typename aren't compatible unfortunately + constexpr inline bool is_empty_vv = sizeof...(Values) == 0; + + // Convenience immediate function overloads for checking if a parameter pack *or* a value pack is empty without different names. + template + consteval bool is_empty_pack() noexcept { return sizeof...(A) == 0; } + + template + consteval bool is_empty_pack() noexcept { return sizeof...(A) == 0; } + + /// Deducable version of `is_empty_pack()` + /// + /// Can be used as `is_empty_pack(pack...)` instead of the slightly more ugly `is_empty_pack()` though with (very unlikely, but possible) implications of actually "evaluating" the pack (despite the values themselves are `const&` and never used, they're still passed.) + /// HOWEVER: The latter should likely be preferred, since this function takes possible-nonconstexpr types as arguments, it cannot be immediate like the non-argument taking overload. See below comment for more information on that. + template + [[gnu::const]] //TODO: XXX: This mostly useless function won't mess up overload resolution for `is_empty_pack()`, right? If it will, either find a way to make sure it comes LAST, or remove it, it's not worth the hassle just so users (i.e. me) can avoid typeing `decltype` for once in my fuckin life... what a stupud keyword name srsl (see static_assert() below.) + constexpr auto is_empty_pack(A const&...) noexcept { return is_empty_pack(); } // This overload exists for: e.g usage for index_sequence: \ + `template f(const auto (&v)[N]) -> \ + template _f(auto const& v, std::index_sequence);`\ + Can be used like: \ + _f(const auto&v, std::convertible_to auto&&... is) \ + { if constexpr(is_empty_pack(is...)) { ... } ... }` \ + i.e: for unknown-typed non-type parameter packs `auto...`s packs can be `forward(pack)...`'d (or just passed, as the taken value is always `const&`) to is_empty_pack(...) and `A...` can therefor be deduced. This is not possible for the non-value taking overloads, and also must be non-consteval since any `A` might not be constexpr. \ +\ + It is a convenience function that is essentially equivelent to `is_empty_pack()`, which should be preferred, since it is consteval and will always be guaranteed to be consteval regardless of the contents of `pack` itself, but I highly doubt any compiler will actually generate any argument-passing code for this overload either. + + static_assert(!is_empty_pack() + and is_empty_pack<>() //XXX: This might also be ambiguous between the type and value one... Eh.... That's actually useful though, so removing this explicit (and pointless) *usage* of the function from the test would be fine if they don't clash in other ways too. + and !is_empty_pack<0, 1, 2>() + and !is_empty_pack(0u, 1.0, 2l) + and is_empty_pack(), "`is_empty_pack()` overload issues."); + template - constexpr auto map(auto const& fun, Args&&... values) noexcept((std::is_nothrow_invocable_v && ...)) - requires((std::is_invocable_v && ...) and ( - (std::is_void_v> || ...) || - std::is_constructible_v...>)) - -> std::conditional_t< (std::is_void_v> || ...) - , void + constexpr auto map(auto const& fun, Args&&... values) noexcept(is_empty_v or is_all_v...>) + requires(is_empty_v or (is_all_v...> and ( + is_any_v>...> || + std::is_constructible_v...>))) + -> std::conditional_t< is_empty_v || is_any_v>...> + , std::void_t...> , R> { - if constexpr( (std::is_void_v> || ...) ) { + if constexpr( is_any_v>...> ) { ((void)std::invoke(fun, values),...); + else if constexpr(sizeof...(Args) == 0) + (void)0; } else return { std::invoke(fun, values)... }; } @@ -197,6 +276,13 @@ namespace exopt { namespace util [[gnu::visibility("internal")]] { template using map_tuple = map, Args...>; + //TODO: template< is_valid_metafunction_with_two_unknown_type_arguments_and_a_possibly_variable_return_type Op, typename... Args> \ + auto reduce(auto const& fun, Op const& comb, Args&&... values) noexcept(...) \ + requires(???) -> std::invoke_result_t { // TODO: Reframe the call to `comb` as an operator overload in a shim class, and use binary fold to make the call, like: ` \ + shim s{comb}; return (s + std::invoke(fun, values) + ...); }` + + + template requires((std::is_invocable_v && ...)) constexpr void apply(const Fn& fun, Args&&... values) noexcept((std::is_nothrow_invocable_v && ...)) /*-> std::conditional_t< (std::is_void_v> || ...)