//! TODO: Document this header, then turn it into a single-header library. It's useful. We can make `src/main.cpp` our test program. //! TODO: Put impl-related stuff into a private namespace maybe? or at least a [[gnu::visibility("internal"/"hidden")]] one? Or perhaps their polymorphism is such that the impl namespace shouldn't be private idk... At most, internal, I'd guess. but it'd need testing. #pragma once #include #include #include #define $_oe_msg(msg) { inline const char* what() const noexcept override { return (msg); } } struct opaque_exception : std::exception $_oe_msg("unspecified opaque_handle related error"); #define $_oe(msg) public opaque_exception $_oe_msg((msg)) struct opaque_not_copyable : $_oe("opaque_handle referred to a type that was not copyable"); struct opaque_handle_moved : $_oe("this opaque_handle was moved"); struct opaque_bad_cast : $_oe("bad opaque_handle pointer cast"); struct opaque_unknown_type : $_oe("handle refers to an unknown or incomplete type"); struct opaque_handle; struct opaque_handle_impl; template constexpr inline bool opaque_is_incomplete = !requires { { sizeof(T) }; }; #define $impl _opaque__impl namespace $impl { template concept CompleteType = !opaque_is_incomplete; } /// A handler that always fails, only really useful for `make_opaque_handle`s that start with null pointers or ones that do not need to be freed and cannot be copied. template constexpr inline auto opaque_empty_handler = [] [[gnu::const, gnu::always_inline]] (auto*, auto) noexcept -> T* { return nullptr; }; struct opaque_handle_impl { friend class opaque_handle; virtual constexpr ~opaque_handle_impl() {} template inline constexpr const To* unsafe_cast() const noexcept { if constexpr(Reinterpret) return reinterpret_cast(as_raw_ptr()); else return static_cast(as_raw_ptr()); } template inline constexpr To* unsafe_cast() noexcept { if constexpr(Reinterpret) return reinterpret_cast(as_raw_ptr()); else return static_cast(as_raw_ptr()); } template inline constexpr const To* try_cast() const noexcept { return is_type() ? static_cast(as_raw_ptr()) : nullptr; } template inline constexpr To* try_cast() noexcept { return is_type() ? static_cast(as_raw_ptr()) : nullptr; } template constexpr bool is_type() const noexcept { if constexpr(requires { { sizeof(T) }; }) return is_type(typeid(T)); else return false; //XXX: Should this be `true`? } constexpr virtual bool is_type(const std::type_info& a) const noexcept { if(const auto* p = get_type_info()) return *p == a; return false; // Types are uncomparable. } protected: constexpr virtual void* as_raw_ptr() const noexcept = 0; constexpr virtual opaque_handle_impl* try_clone() const = 0; constexpr virtual const std::type_info* get_type_info() const noexcept =0; inline virtual opaque_handle_impl& clone() const { if(auto* np = try_clone()) return *np; throw opaque_not_copyable{}; } constexpr opaque_handle_impl() noexcept{} }; struct opaque_handle final { enum cast_kind { CAST_SAFE, CAST_UNSAFE, CAST_UNSAFE_REINTERPRET, }; constexpr explicit opaque_handle(opaque_handle_impl* ptr) noexcept : _impl(ptr){} constexpr opaque_handle(opaque_handle&& move) noexcept : _impl(std::exchange(move._impl, nullptr)){} inline opaque_handle(const opaque_handle& copy) : _impl(copy._impl ? copy._impl->try_clone() //(copy._impl->try_clone() ?: throw opaque_not_copyable{}) : nullptr){} constexpr ~opaque_handle() { if(_impl) delete _impl; } constexpr const void* as_ptr() const noexcept { return _impl ? _impl->as_raw_ptr() : nullptr; } constexpr void* as_ptr() noexcept { return _impl ? _impl->as_raw_ptr() : nullptr; } template constexpr const T* unsafe_cast() const noexcept { return _impl ? _impl->unsafe_cast() : nullptr; } template constexpr T* unsafe_cast() noexcept { return _impl ? _impl->unsafe_cast() : nullptr; } /// WARNING: try_cast(), where T is an incomplete type, will always succeed, which may lead to undefined behaviour. Make sure to account for this template constexpr const T* try_cast() const noexcept { static_assert(!opaque_is_incomplete, "try_cast always returns nullptr when T is an incomplete type, use `ptr_cast()` if T may be incomplete, or `unsafe_cast()` is it is known to be incomplete"); return _impl ? _impl->try_cast() : nullptr; } template constexpr T* try_cast() noexcept { static_assert(!opaque_is_incomplete, "try_cast always returns nullptr when T is an incomplete type, use `ptr_cast()` if T may be incomplete, or `unsafe_cast()` if it is known to be incomplete."); return _impl ? _impl->try_cast() : nullptr; } template<$impl::CompleteType T> constexpr bool is_type() const noexcept { return is_type(typeid(T)); } constexpr bool is_type(const std::type_info& ty) const noexcept { return _impl ? _impl->is_type(ty) : false; } constexpr const std::type_info* try_get_type() const noexcept { return _impl ? _impl->get_type_info() : nullptr; } constexpr const std::type_info& get_type() const { if(__builtin_expect(_impl == nullptr, false)) throw opaque_handle_moved{}; return *(_impl->get_type_info() ?: throw opaque_unknown_type{}); } /// ptr_cast(): use try_cast() for complete types, and unsafe_cast() for incomplete types. template constexpr T* ptr_cast() noexcept { if constexpr(opaque_is_incomplete) return unsafe_cast(); else return try_cast(); } template constexpr const T* ptr_cast() const noexcept { if constexpr(opaque_is_incomplete) return unsafe_cast(); else return try_cast(); } template<$impl::CompleteType T, cast_kind Kind = CAST_SAFE> inline const T& cast() const { if constexpr(Kind >= CAST_UNSAFE) return *(unsafe_cast() ?: throw opaque_bad_cast{}); else { // CAST_SAFE return *(try_cast() ?: throw opaque_bad_cast{}); } } template<$impl::CompleteType T, cast_kind Kind = CAST_SAFE> inline T& cast() { if constexpr(Kind >= CAST_UNSAFE) return *(unsafe_cast() ?: throw opaque_bad_cast{}); else return *(try_cast() ?: throw opaque_bad_cast{}); } constexpr bool has_value() const noexcept { return bool(_impl); } constexpr explicit operator bool() const noexcept { return has_value(); } // XXX: NOTE: operator-> and operator* do not, and SHOULD NOT, work for incomplete types. only `ptr_cast()` and `operator [const] T*()` should consider the completeness of the type. //TODO: Test these, we may need to do the same thing we did for `operator*`, but with a shim that has `operator T*` and `operator->` instead of `operator T&`. template constexpr const T* operator->() const noexcept(std::is_void_v) { if constexpr(std::is_void_v) return as_ptr(); else return std::addressof(cast()); } template constexpr T* operator->() noexcept(std::is_void_v) { if constexpr(std::is_void_v) return as_ptr(); else return std::addressof(cast()); } [[gnu::artificial]] inline constexpr auto operator*() const noexcept { return _impl_deref_const{*this}; } [[gnu::artificial]] inline constexpr auto operator*() noexcept { return _impl_deref{*this}; } /* template inline auto operator*() noexcept { return *(try_cast() ?: throw opaque_bad_cast{}); } */ template constexpr operator T*() noexcept { return ptr_cast(); } template constexpr operator const T*() const noexcept { return ptr_cast(); } private: struct _impl_deref_const { const opaque_handle& ptr; template [[gnu::artificial, gnu::always_inline]] inline operator const T&() && { return ptr.cast(); } template [[gnu::artificial, gnu::always_inline]] inline operator T() const&& { // For this to work, this method must return `T`, i'm not sure *why*, but oh well. It doesn't seem to incur any extra overhead, so... okay. return ptr.cast(); } }; struct _impl_deref { opaque_handle& ptr; template inline operator T&() && { return ptr.cast(); //*(ptr.try_cast() ?: throw opaque_bad_cast{}); } template inline operator const T&() const&& { return ptr.cast(); //*(ptr.try_cast() ?: throw opaque_bad_cast{}); } }; opaque_handle_impl* _impl; }; template struct opaque_handle_object : public opaque_handle_impl { friend class opaque_handle; inline constexpr opaque_handle_object(T&& value) noexcept(std::is_nothrow_move_constructible_v) : _obj(std::move(value)){} inline constexpr virtual ~opaque_handle_object() {} protected: inline constexpr void* as_raw_ptr() const noexcept override { return const_cast(static_cast(&_obj) /*?: (static_cast(&_obj) ?: &_obj)*/); } inline constexpr opaque_handle_impl* try_clone() const override { if constexpr(std::is_copy_constructible_v) { return new opaque_handle_object{T{_obj}}; } else return nullptr; } inline constexpr const std::type_info* get_type_info() const noexcept override { return &typeid(T); } private: T _obj; }; template opaque_handle_object(T&&) -> opaque_handle_object; /// Make an opaque_handle from an object. template constexpr inline opaque_handle make_opaque_object_handle(T&& value) noexcept(std::is_nothrow_move_constructible_v) { return opaque_handle{static_cast(new opaque_handle_object(std::move(value)))}; } /// Make an opaque_handle from a newly constructed object `T`. template requires(std::is_constructible_v) constexpr inline opaque_handle make_opaque_object_handle(Args&&... ctor) noexcept(std::is_nothrow_constructible_v) { return opaque_handle{static_cast(new opaque_handle_object(std::forward(ctor)...))}; } /// Operation for `OpaqueHandleFunc` functions. enum class opaque_handle_operation { Clone, Delete, }; /// A function that handles the de-allocation and cloning (if supported) of an opaque_handle created with `make_opaque_handle`. template concept OpaqueHandleFunc = std::is_invocable_v && std::is_convertible_v, T*>; /// Create and opaque_handle from a data pointer and a lambda which handles the copying (if possible) and deleting of the object /// /// `handler` should be in the form: `auto* (T*, opaque_handle_operation) [noexcept]` and the reference must outlive the returned object. It should handle a `null` argument, and it can return `null`. template HandleF> constexpr inline opaque_handle make_opaque_handle(T* data, const HandleF& handler) noexcept { #define is_nothrow (std::is_nothrow_invocable_v) struct object_handler final : opaque_handle_impl { constexpr object_handler(const object_handler& c) noexcept(is_nothrow) : data(c.handler(c.data, opaque_handle_operation::Clone)) , handler(c.handler){} constexpr object_handler(object_handler&& m) noexcept : data(std::exchange(m.data, nullptr)) , handler(m.handler){} constexpr explicit object_handler(T* _data, const HandleF& handler) noexcept : data(_data) , handler(handler){} constexpr virtual ~object_handler() //noexcept(is_nothrow) { if(data) (void)handler(data, opaque_handle_operation::Delete); } inline constexpr void* as_raw_ptr() const noexcept override final { return const_cast(data); } constexpr opaque_handle_impl* try_clone() const override final { return data ? new object_handler(*this) : nullptr; } inline constexpr const std::type_info* get_type_info() const noexcept override final { if constexpr(requires { { sizeof(T) }; }) return &typeid(T); else return nullptr; } T* data; const HandleF& handler; }; return opaque_handle(static_cast(new object_handler(data, handler))); #undef is_nothrow } /// Create and opaque_handle from a data pointer and a move-constructible functor which handles the copying (if possible) and deleting of the object. /// /// `handler` should be in the form: `auto* (T*, opaque_handle_operation) [noexcept]`. Its lifetime is managed by the returned opaque_handle template HandleF> constexpr inline opaque_handle make_opaque_handle(T* data, HandleF&& handler) noexcept(std::is_nothrow_move_constructible_v) { #define is_nothrow (std::is_nothrow_invocable_v) #define is_nothrow_ctor (std::is_nothrow_move_constructible_v) struct object_handler final : opaque_handle_impl { constexpr object_handler(const object_handler& c) noexcept(is_nothrow && std::is_nothrow_copy_constructible_v) requires(std::is_copy_constructible_v) : data(c.handler(c.data, opaque_handle_operation::Clone)) , handler(c.handler){} constexpr object_handler(object_handler&& m) noexcept : data(std::exchange(m.data, nullptr)) , handler(std::move(m.handler)){} constexpr explicit object_handler(T* _data, HandleF&& handler) noexcept(is_nothrow_ctor) : data(_data) , handler(std::move(handler)){} constexpr virtual ~object_handler() //noexcept(is_nothrow) { if(data) (void)handler(data, opaque_handle_operation::Delete); } inline constexpr void* as_raw_ptr() const noexcept override final { return const_cast(data); } constexpr opaque_handle_impl* try_clone() const override final { if constexpr(std::is_copy_constructible_v) { return data ? new object_handler(*this) : nullptr; } else return nullptr; } inline constexpr const std::type_info* get_type_info() const noexcept override final { if constexpr(requires { { sizeof(T) }; }) return &typeid(T); else return nullptr; } T* data; HandleF handler; }; return opaque_handle(static_cast(new object_handler(data, std::forward(handler)))); #undef is_nothrow #undef is_nothrow_ctor } /// Create and opaque_handle from a data pointer and a constexpr handler funcion template param (e.g. a lambda) /// /// `Func` should be in the form: `auto* (T*, opaque_handle_operation) [noexcept]`. It must be a constant expression. template auto Func> constexpr inline opaque_handle make_opaque_handle(T* data) noexcept { using HandleF = decltype(Func); #define is_nothrow (std::is_nothrow_invocable_v) struct object_handler final : opaque_handle_impl { constexpr object_handler(const object_handler& c) noexcept(is_nothrow) : data(Func(c.data, opaque_handle_operation::Clone)){} constexpr object_handler(object_handler&& m) noexcept : data(std::exchange(m.data, nullptr)){} constexpr explicit object_handler(T* data) noexcept : data(data){} constexpr virtual ~object_handler() //noexcept(is_nothrow) { if(data) (void)Func(data, opaque_handle_operation::Delete); } inline constexpr void* as_raw_ptr() const noexcept override final { return const_cast(data); } constexpr opaque_handle_impl* try_clone() const override final { return data ? new object_handler(*this) : nullptr; } inline constexpr const std::type_info* get_type_info() const noexcept override final { if constexpr(requires { { sizeof(T) }; }) return &typeid(T); else return nullptr; } T* data; }; return opaque_handle(static_cast(new object_handler(data))); #undef is_nothrow } #undef $_oe_msg #undef $_oe #undef $impl