You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
16 KiB

//! 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 <utility>
#include <stdexcept>
#include <typeinfo>
#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<typename T>
constexpr inline bool opaque_is_incomplete = !requires { { sizeof(T) }; };
#define $impl _opaque__impl
namespace $impl {
template<typename T>
concept CompleteType = !opaque_is_incomplete<T>;
}
/// 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<typename T>
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<typename To, bool Reinterpret=false>
inline constexpr const To* unsafe_cast() const noexcept {
if constexpr(Reinterpret)
return reinterpret_cast<const To*>(as_raw_ptr());
else return static_cast<const To*>(as_raw_ptr());
}
template<typename To, bool Reinterpret=false>
inline constexpr To* unsafe_cast() noexcept {
if constexpr(Reinterpret)
return reinterpret_cast<To*>(as_raw_ptr());
else return static_cast<To*>(as_raw_ptr());
}
template<typename To>
inline constexpr const To* try_cast() const noexcept { return is_type<To>() ? static_cast<const To*>(as_raw_ptr()) : nullptr; }
template<typename To>
inline constexpr To* try_cast() noexcept { return is_type<To>() ? static_cast<To*>(as_raw_ptr()) : nullptr; }
template<typename T>
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<typename T, bool Reinterpret=false>
constexpr const T* unsafe_cast() const noexcept { return _impl ? _impl->unsafe_cast<T, Reinterpret>() : nullptr; }
template<typename T, bool Reinterpret=false>
constexpr T* unsafe_cast() noexcept { return _impl ? _impl->unsafe_cast<T, Reinterpret>() : nullptr; }
/// WARNING: try_cast<T>(), where T is an incomplete type, will always succeed, which may lead to undefined behaviour. Make sure to account for this
template<typename T>
constexpr const T* try_cast() const noexcept { static_assert(!opaque_is_incomplete<T>, "try_cast<T> 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<T>() : nullptr; }
template<typename T>
constexpr T* try_cast() noexcept { static_assert(!opaque_is_incomplete<T>, "try_cast<T> 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<T>() : 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<typename T>
constexpr T* ptr_cast() noexcept {
if constexpr(opaque_is_incomplete<T>) return unsafe_cast<T>();
else return try_cast<T>();
}
template<typename T>
constexpr const T* ptr_cast() const noexcept {
if constexpr(opaque_is_incomplete<T>) return unsafe_cast<T>();
else return try_cast<T>();
}
template<$impl::CompleteType T, cast_kind Kind = CAST_SAFE>
inline const T& cast() const {
if constexpr(Kind >= CAST_UNSAFE)
return *(unsafe_cast<T, (Kind == CAST_UNSAFE_REINTERPRET)>() ?: throw opaque_bad_cast{});
else { // CAST_SAFE
return *(try_cast<T>() ?: throw opaque_bad_cast{});
}
}
template<$impl::CompleteType T, cast_kind Kind = CAST_SAFE>
inline T& cast() {
if constexpr(Kind >= CAST_UNSAFE)
return *(unsafe_cast<T, (Kind == CAST_UNSAFE_REINTERPRET)>() ?: throw opaque_bad_cast{});
else
return *(try_cast<T>() ?: 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<typename T>
constexpr const T* operator->() const noexcept(std::is_void_v<T>) {
if constexpr(std::is_void_v<T>)
return as_ptr();
else return std::addressof(cast<T>());
}
template<typename T>
constexpr T* operator->() noexcept(std::is_void_v<T>)
{
if constexpr(std::is_void_v<T>)
return as_ptr();
else return std::addressof(cast<T>());
}
[[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<typename T>
inline auto operator*() noexcept { return *(try_cast<T>() ?: throw opaque_bad_cast{}); }
*/
template<typename T>
constexpr operator T*() noexcept {
return ptr_cast<T>();
}
template<typename T>
constexpr operator const T*() const noexcept {
return ptr_cast<T>();
}
private:
struct _impl_deref_const {
const opaque_handle& ptr;
template<typename T>
[[gnu::artificial, gnu::always_inline]]
inline operator const T&() && {
return ptr.cast<T>();
}
template<typename T>
[[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<T>();
}
};
struct _impl_deref {
opaque_handle& ptr;
template<typename T>
inline operator T&() && {
return ptr.cast<T>(); //*(ptr.try_cast<T>() ?: throw opaque_bad_cast{});
}
template<typename T>
inline operator const T&() const&& {
return ptr.cast<T>(); //*(ptr.try_cast<T>() ?: throw opaque_bad_cast{});
}
};
opaque_handle_impl* _impl;
};
template<typename T>
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<T>)
: _obj(std::move(value)){}
inline constexpr virtual ~opaque_handle_object() {}
protected:
inline constexpr void* as_raw_ptr() const noexcept override { return const_cast<void*>(static_cast<const void*>(&_obj) /*?: (static_cast<void*>(&_obj) ?: &_obj)*/); }
inline constexpr opaque_handle_impl* try_clone() const override {
if constexpr(std::is_copy_constructible_v<T>) {
return new opaque_handle_object<T>{T{_obj}};
} else return nullptr;
}
inline constexpr const std::type_info* get_type_info() const noexcept override { return &typeid(T); }
private:
T _obj;
};
template<typename T>
opaque_handle_object(T&&) -> opaque_handle_object<T>;
/// Make an opaque_handle from an object.
template<typename T>
constexpr inline opaque_handle make_opaque_object_handle(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
{
return opaque_handle{static_cast<opaque_handle_impl*>(new opaque_handle_object<T>(std::move(value)))};
}
/// Make an opaque_handle from a newly constructed object `T`.
template<typename T, typename... Args> requires(std::is_constructible_v<T, Args...>)
constexpr inline opaque_handle make_opaque_object_handle(Args&&... ctor) noexcept(std::is_nothrow_constructible_v<T, Args...>)
{
return opaque_handle{static_cast<opaque_handle_impl*>(new opaque_handle_object<T>(std::forward<Args>(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<typename F, typename T>
concept OpaqueHandleFunc = std::is_invocable_v<F, T*, opaque_handle_operation>
&& std::is_convertible_v<std::invoke_result_t<F, T*, opaque_handle_operation>, 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<typename T, OpaqueHandleFunc<T> HandleF>
constexpr inline opaque_handle make_opaque_handle(T* data, const HandleF& handler) noexcept
{
#define is_nothrow (std::is_nothrow_invocable_v<HandleF, T*, opaque_handle_operation>)
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<T*>(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<opaque_handle_impl*>(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<typename T, OpaqueHandleFunc<T> HandleF>
constexpr inline opaque_handle make_opaque_handle(T* data, HandleF&& handler) noexcept(std::is_nothrow_move_constructible_v<HandleF>)
{
#define is_nothrow (std::is_nothrow_invocable_v<HandleF, T*, opaque_handle_operation>)
#define is_nothrow_ctor (std::is_nothrow_move_constructible_v<HandleF>)
struct object_handler final : opaque_handle_impl
{
constexpr object_handler(const object_handler& c)
noexcept(is_nothrow && std::is_nothrow_copy_constructible_v<HandleF>) requires(std::is_copy_constructible_v<HandleF>)
: 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<T*>(data); }
constexpr opaque_handle_impl* try_clone() const override final
{
if constexpr(std::is_copy_constructible_v<HandleF>) {
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<opaque_handle_impl*>(new object_handler(data, std::forward<HandleF>(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<typename T, OpaqueHandleFunc<T> auto Func>
constexpr inline opaque_handle make_opaque_handle(T* data) noexcept
{
using HandleF = decltype(Func);
#define is_nothrow (std::is_nothrow_invocable_v<HandleF, T*, opaque_handle_operation>)
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<T*>(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<opaque_handle_impl*>(new object_handler(data)));
#undef is_nothrow
}
#undef $_oe_msg
#undef $_oe
#undef $impl