872 lines
44 KiB
C++
872 lines
44 KiB
C++
#pragma once
|
|
#include "type_descriptor.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <concepts>
|
|
#include <cstddef>
|
|
#include <functional>
|
|
#include <initializer_list>
|
|
#include <memory>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <typeinfo>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
namespace structive {
|
|
struct Null_Shared_Mutex {
|
|
void lock() {}
|
|
void unlock() {}
|
|
void lock_shared() {}
|
|
void unlock_shared() {}
|
|
};
|
|
struct Shared_Mutex_Policy {
|
|
using mutex_type = std::shared_mutex;
|
|
};
|
|
struct No_Lock_Policy {
|
|
using mutex_type = Null_Shared_Mutex;
|
|
};
|
|
template <class Mutex>
|
|
concept Shared_Lockable = requires(Mutex& mutex) {
|
|
mutex.lock();
|
|
mutex.unlock();
|
|
mutex.lock_shared();
|
|
mutex.unlock_shared();
|
|
};
|
|
template <class Policy>
|
|
concept Synchronization_Policy = requires {
|
|
typename Policy::mutex_type;
|
|
} && Shared_Lockable<typename Policy::mutex_type>;
|
|
namespace detail {
|
|
template <class Mutex, bool Stores_Mutexes = !std::same_as<Mutex, Null_Shared_Mutex>>
|
|
class Mutex_Storage;
|
|
template <class Mutex>
|
|
class Mutex_Storage<Mutex, true> {
|
|
std::unique_ptr<Mutex[]> mutexes_;
|
|
public:
|
|
Mutex_Storage() = default;
|
|
explicit Mutex_Storage(std::size_t count) : mutexes_(count ? std::make_unique<Mutex[]>(count) : nullptr) {}
|
|
void reset(std::size_t count) {
|
|
mutexes_ = count ? std::make_unique<Mutex[]>(count) : nullptr;
|
|
}
|
|
Mutex& get(std::size_t index) noexcept {
|
|
return mutexes_[index];
|
|
}
|
|
Mutex& get(std::size_t index) const noexcept {
|
|
return mutexes_[index];
|
|
}
|
|
};
|
|
template <class Mutex>
|
|
class Mutex_Storage<Mutex, false> {
|
|
static Mutex& mutex() noexcept {
|
|
static Mutex value;
|
|
return value;
|
|
}
|
|
public:
|
|
Mutex_Storage() = default;
|
|
explicit Mutex_Storage(std::size_t) {}
|
|
void reset(std::size_t) {}
|
|
Mutex& get(std::size_t) noexcept {
|
|
return mutex();
|
|
}
|
|
Mutex& get(std::size_t) const noexcept {
|
|
return mutex();
|
|
}
|
|
};
|
|
}
|
|
/// Result of type-erased runtime property access. Runtime lookup never performs implicit type conversion.
|
|
enum class Runtime_Access_Result {
|
|
ok,
|
|
unknown_property,
|
|
not_readable,
|
|
not_writable,
|
|
unsupported_runtime_write,
|
|
type_mismatch
|
|
};
|
|
/// Callback used by `runtime_read`.
|
|
/// The value pointer is valid only for the duration of the callback and must be consumed or copied synchronously. For synchronized writable state, the corresponding managed read lock remains held while the callback executes, so the callback must not re-enter a conflicting write on the same lock domain.
|
|
using Runtime_Read_Callback = void (*)(void*, std::size_t, std::string_view, const std::type_info&, const void*);
|
|
/// Type-erased boundary intended for adapters that discover a property key only at runtime.
|
|
/// Normal C++ business code should prefer `Property_Object::read/write`, which preserve compile-time type and capability checking.
|
|
class Property_Object_Base {
|
|
struct Runtime_Interface {
|
|
const std::type_info& (*object_type)() noexcept;
|
|
std::size_t (*property_count)() noexcept;
|
|
Runtime_Access_Result (*read)(const Property_Object_Base&, std::string_view, void*, Runtime_Read_Callback);
|
|
Runtime_Access_Result (*write)(Property_Object_Base&, std::string_view, const std::type_info&, const void*);
|
|
};
|
|
const Runtime_Interface* runtime_interface_{};
|
|
protected:
|
|
explicit Property_Object_Base(const Runtime_Interface* runtime_interface) noexcept : runtime_interface_(runtime_interface) {}
|
|
Property_Object_Base(const Property_Object_Base&) noexcept = default;
|
|
Property_Object_Base(Property_Object_Base&&) noexcept = default;
|
|
Property_Object_Base& operator=(const Property_Object_Base&) noexcept = default;
|
|
Property_Object_Base& operator=(Property_Object_Base&&) noexcept = default;
|
|
~Property_Object_Base() = default;
|
|
template <class Object, Synchronization_Policy Policy>
|
|
friend class Property_Object;
|
|
public:
|
|
/// Returns the concrete managed object type represented by this erased base.
|
|
const std::type_info& runtime_object_type() const noexcept {
|
|
return runtime_interface_->object_type();
|
|
}
|
|
/// Returns the number of properties in the concrete object's schema.
|
|
std::size_t runtime_property_count() const noexcept {
|
|
return runtime_interface_->property_count();
|
|
}
|
|
/// Looks up `key` at runtime and invokes `callback` exactly once on success.
|
|
/// The operation follows the same intrinsic readable capability and synchronization rules as typed `read`; stored read-only properties therefore keep their zero-lock fast path. `callback` is a required non-null function pointer.
|
|
Runtime_Access_Result runtime_read(std::string_view key, void* context, Runtime_Read_Callback callback) const {
|
|
return runtime_interface_->read(*this, key, context, callback);
|
|
}
|
|
/// Looks up `key` at runtime and performs a copy-input managed write.
|
|
/// `value_type` must exactly match the property's declared value type and `value` must point to a live object of that exact type for the duration of the call. No numeric, string or user-defined conversion is attempted. Intrinsically writable properties that require move-only input report `unsupported_runtime_write`; typed `write` remains available for them.
|
|
Runtime_Access_Result runtime_write(std::string_view key, const std::type_info& value_type, const void* value) {
|
|
return runtime_interface_->write(*this, key, value_type, value);
|
|
}
|
|
};
|
|
/// Explicit per-instance synchronization override. The contained plan changes only lock topology; it does not change property capability or validation.
|
|
struct Property_Synchronization {
|
|
Synchronization_Plan plan;
|
|
};
|
|
inline Property_Synchronization property_synchronization(Synchronization_Plan plan) {
|
|
return {std::move(plan)};
|
|
}
|
|
template <class Object, Synchronization_Source_Type Source>
|
|
Property_Synchronization property_synchronization(Source&& source) requires Property_Described_Object<Object> {
|
|
using Schema = type_descriptor_schema_t<Object>;
|
|
return {materialize_synchronization_plan<Schema>(std::forward<Source>(source))};
|
|
}
|
|
/// Non-owning inspection view of the effective synchronization topology for one object instance.
|
|
/// `unsynchronized_slot` means that managed access for the property owns no mutex domain. For stored read-only properties this is automatic.
|
|
struct Resolved_Synchronization_View {
|
|
static constexpr std::size_t unsynchronized_slot = std::numeric_limits<std::size_t>::max();
|
|
std::span<const std::size_t> lock_slots;
|
|
std::size_t lock_count{};
|
|
std::size_t slot(std::size_t index) const noexcept {
|
|
return lock_slots[index];
|
|
}
|
|
bool uses_lock(std::size_t index) const noexcept {
|
|
return slot(index) != unsynchronized_slot;
|
|
}
|
|
};
|
|
template <class Derived, Synchronization_Policy Lock_Policy = Shared_Mutex_Policy>
|
|
class Property_Object : public Property_Object_Base {
|
|
public:
|
|
using object_type = Derived;
|
|
using mutex_type = typename Lock_Policy::mutex_type;
|
|
private:
|
|
static constexpr bool uses_real_mutexes = !std::same_as<mutex_type, Null_Shared_Mutex>;
|
|
struct Empty_Guard_Locks {};
|
|
struct Dynamic_Lock_Targets {
|
|
std::vector<std::size_t> slots;
|
|
std::vector<std::size_t> unsynchronized_properties;
|
|
};
|
|
template <std::size_t Capacity>
|
|
struct Static_Lock_Targets {
|
|
std::array<std::size_t, Capacity> slots{};
|
|
std::size_t slot_count{};
|
|
std::array<std::size_t, Capacity> unsynchronized_properties{};
|
|
std::size_t unsynchronized_count{};
|
|
};
|
|
std::unique_ptr<std::size_t[]> custom_lock_layout_;
|
|
[[no_unique_address]] detail::Mutex_Storage<mutex_type> mutex_storage_;
|
|
static const auto& default_resolved_synchronization() {
|
|
static const auto resolved = resolve_synchronization_plan(type_descriptor<Derived>(), type_descriptor<Derived>().synchronization_plan());
|
|
return resolved;
|
|
}
|
|
template <class Schema>
|
|
void initialize(const Schema& schema, const Synchronization_Plan& plan) {
|
|
auto resolved = resolve_synchronization_plan(schema, plan);
|
|
auto layout = std::make_unique<std::size_t[]>(Schema::property_count + 1);
|
|
layout[0] = resolved.lock_count;
|
|
std::copy(resolved.lock_slots.begin(), resolved.lock_slots.end(), layout.get() + 1);
|
|
custom_lock_layout_ = std::move(layout);
|
|
mutex_storage_.reset(resolved.lock_count);
|
|
}
|
|
void copy_synchronization_from(const Property_Object& other) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
if (other.custom_lock_layout_) {
|
|
auto layout = std::make_unique<std::size_t[]>(Schema::property_count + 1);
|
|
std::copy_n(other.custom_lock_layout_.get(), Schema::property_count + 1, layout.get());
|
|
custom_lock_layout_ = std::move(layout);
|
|
} else {
|
|
custom_lock_layout_.reset();
|
|
}
|
|
mutex_storage_.reset(lock_count());
|
|
}
|
|
std::size_t lock_count() const noexcept {
|
|
return custom_lock_layout_ ? custom_lock_layout_[0] : default_resolved_synchronization().lock_count;
|
|
}
|
|
std::size_t slot(std::size_t index) const noexcept {
|
|
return custom_lock_layout_ ? custom_lock_layout_[index + 1] : default_resolved_synchronization().slot(index);
|
|
}
|
|
mutex_type& mutex(std::size_t index) noexcept {
|
|
return mutex_storage_.get(index);
|
|
}
|
|
mutex_type& mutex(std::size_t index) const noexcept {
|
|
return mutex_storage_.get(index);
|
|
}
|
|
template <std::size_t Capacity>
|
|
static void sort_unique_prefix(std::array<std::size_t, Capacity>& values, std::size_t& count) {
|
|
for (std::size_t index = 1; index < count; ++index) {
|
|
auto value = values[index];
|
|
auto position = index;
|
|
while (position > 0 && value < values[position - 1]) {
|
|
values[position] = values[position - 1];
|
|
--position;
|
|
}
|
|
values[position] = value;
|
|
}
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
std::size_t write = 1;
|
|
for (std::size_t read = 1; read < count; ++read) {
|
|
if (values[read] != values[write - 1]) {
|
|
values[write++] = values[read];
|
|
}
|
|
}
|
|
count = write;
|
|
}
|
|
static bool runtime_property_readable(std::size_t index) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static const auto table = []<std::size_t... Index>(std::index_sequence<Index...>) {
|
|
return std::array<bool, Schema::property_count>{Schema::template property_type<Index>::readable...};
|
|
}(std::make_index_sequence<Schema::property_count>{});
|
|
return table[index];
|
|
}
|
|
static bool runtime_property_writable(std::size_t index) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static const auto table = []<std::size_t... Index>(std::index_sequence<Index...>) {
|
|
return std::array<bool, Schema::property_count>{Schema::template property_type<Index>::writable...};
|
|
}(std::make_index_sequence<Schema::property_count>{});
|
|
return table[index];
|
|
}
|
|
Dynamic_Lock_Targets collect_dynamic_lock_targets(std::span<const std::string_view> keys, bool shared_access) const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
Dynamic_Lock_Targets targets;
|
|
targets.slots.reserve(keys.size());
|
|
targets.unsynchronized_properties.reserve(keys.size());
|
|
for (auto key : keys) {
|
|
auto index = schema_property_index<Schema>(key);
|
|
if (!index) {
|
|
throw std::invalid_argument("Unknown property: " + std::string(key));
|
|
}
|
|
bool allowed = shared_access ? runtime_property_readable(*index) : runtime_property_writable(*index);
|
|
if (!allowed) {
|
|
throw std::invalid_argument(std::string(shared_access ? "Property is not readable: " : "Property is not writable: ") + std::string(key));
|
|
}
|
|
auto lock_slot = slot(*index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
targets.unsynchronized_properties.push_back(*index);
|
|
} else {
|
|
targets.slots.push_back(lock_slot);
|
|
}
|
|
}
|
|
std::sort(targets.slots.begin(), targets.slots.end());
|
|
targets.slots.erase(std::unique(targets.slots.begin(), targets.slots.end()), targets.slots.end());
|
|
std::sort(targets.unsynchronized_properties.begin(), targets.unsynchronized_properties.end());
|
|
targets.unsynchronized_properties.erase(std::unique(targets.unsynchronized_properties.begin(), targets.unsynchronized_properties.end()), targets.unsynchronized_properties.end());
|
|
return targets;
|
|
}
|
|
template <bool Shared_Access, std::size_t... Index>
|
|
Static_Lock_Targets<sizeof...(Index)> collect_static_lock_targets() const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static_assert(((Shared_Access ? Schema::template property_type<Index>::readable : Schema::template property_type<Index>::writable) && ...));
|
|
Static_Lock_Targets<sizeof...(Index)> targets;
|
|
auto collect = [&]<std::size_t Property_Index>() {
|
|
auto lock_slot = slot(Property_Index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
targets.unsynchronized_properties[targets.unsynchronized_count++] = Property_Index;
|
|
} else {
|
|
targets.slots[targets.slot_count++] = lock_slot;
|
|
}
|
|
};
|
|
(collect.template operator()<Index>(), ...);
|
|
sort_unique_prefix(targets.slots, targets.slot_count);
|
|
sort_unique_prefix(targets.unsynchronized_properties, targets.unsynchronized_count);
|
|
return targets;
|
|
}
|
|
template <bool Shared_Access>
|
|
auto collect_all_static_lock_targets() const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
Static_Lock_Targets<Schema::property_count> targets;
|
|
auto collect = [&]<std::size_t Index>() {
|
|
constexpr bool allowed = Shared_Access ? Schema::template property_type<Index>::readable : Schema::template property_type<Index>::writable;
|
|
if constexpr (allowed) {
|
|
auto lock_slot = slot(Index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
targets.unsynchronized_properties[targets.unsynchronized_count++] = Index;
|
|
} else {
|
|
targets.slots[targets.slot_count++] = lock_slot;
|
|
}
|
|
}
|
|
};
|
|
[&]<std::size_t... Index>(std::index_sequence<Index...>) {
|
|
(collect.template operator()<Index>(), ...);
|
|
}(std::make_index_sequence<Schema::property_count>{});
|
|
sort_unique_prefix(targets.slots, targets.slot_count);
|
|
sort_unique_prefix(targets.unsynchronized_properties, targets.unsynchronized_count);
|
|
return targets;
|
|
}
|
|
template <std::size_t Index, class View>
|
|
decltype(auto) read_unlocked(const View& view) const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
const auto& descriptor = type_descriptor<Derived>().template property<Index>();
|
|
using Accessor = typename Schema::template property_type<Index>::accessor_type;
|
|
if constexpr (Accessor::synchronized_view_read) {
|
|
return descriptor.accessor.read(view);
|
|
} else {
|
|
return descriptor.accessor.read(static_cast<const Derived&>(*this));
|
|
}
|
|
}
|
|
template <std::size_t Index, class Value>
|
|
void write_unlocked(Value&& value) {
|
|
const auto& descriptor = type_descriptor<Derived>().template property<Index>();
|
|
descriptor.accessor.write(static_cast<Derived&>(*this), std::forward<Value>(value));
|
|
}
|
|
template <std::size_t Property_Index>
|
|
class Single_Read_View {
|
|
const Property_Object* owner_{};
|
|
std::size_t lock_slot_{};
|
|
template <std::size_t Index>
|
|
decltype(auto) get_index() const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
using Property = typename Schema::template property_type<Index>;
|
|
static_assert(schema_property_depends_on_index_v<Schema, Property_Index, Index>, "computed property reads an undeclared dependency");
|
|
if constexpr (Property::accessor_type::synchronized_view_read) {
|
|
Single_Read_View<Index> nested{*owner_, lock_slot_};
|
|
return owner_->template read_unlocked<Index>(nested);
|
|
} else {
|
|
return owner_->template read_unlocked<Index>(*this);
|
|
}
|
|
}
|
|
public:
|
|
Single_Read_View(const Property_Object& owner, std::size_t lock_slot) : owner_(&owner), lock_slot_(lock_slot) {}
|
|
template <auto Member>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return get_index<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return get_index<index>();
|
|
}
|
|
};
|
|
template <std::size_t Index>
|
|
auto read_one() const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static_assert(Schema::template property_type<Index>::readable);
|
|
using Property = typename Schema::template property_type<Index>;
|
|
using Value = typename Property::value_type;
|
|
if constexpr ((!Property::writable && !Property::accessor_type::synchronized_view_read) || (!uses_real_mutexes && !Property::accessor_type::synchronized_view_read)) {
|
|
return Value(read_unlocked<Index>(*this));
|
|
}
|
|
auto lock_slot = slot(Index);
|
|
Single_Read_View<Index> view{*this, lock_slot};
|
|
if constexpr (!uses_real_mutexes) {
|
|
return Value(read_unlocked<Index>(view));
|
|
}
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
return Value(read_unlocked<Index>(view));
|
|
}
|
|
std::shared_lock lock{mutex(lock_slot)};
|
|
return Value(read_unlocked<Index>(view));
|
|
}
|
|
template <std::size_t Index, class Value>
|
|
void write_one(Value&& value) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static_assert(Schema::template property_type<Index>::writable);
|
|
if constexpr (!uses_real_mutexes) {
|
|
write_unlocked<Index>(std::forward<Value>(value));
|
|
return;
|
|
}
|
|
auto lock_slot = slot(Index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
write_unlocked<Index>(std::forward<Value>(value));
|
|
return;
|
|
}
|
|
std::unique_lock lock{mutex(lock_slot)};
|
|
write_unlocked<Index>(std::forward<Value>(value));
|
|
}
|
|
private:
|
|
class Basic_Read_Guard {
|
|
const Property_Object* owner_{};
|
|
std::vector<std::size_t> slots_;
|
|
std::vector<std::size_t> unsynchronized_properties_;
|
|
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::vector<std::shared_lock<mutex_type>>, Empty_Guard_Locks> locks_;
|
|
bool holds(std::size_t index) const {
|
|
auto lock_slot = owner_->slot(index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
return std::binary_search(unsynchronized_properties_.begin(), unsynchronized_properties_.end(), index);
|
|
}
|
|
return std::binary_search(slots_.begin(), slots_.end(), lock_slot);
|
|
}
|
|
friend class Property_Object;
|
|
Basic_Read_Guard(const Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) {
|
|
if constexpr (uses_real_mutexes) {
|
|
locks_.reserve(slots_.size());
|
|
for (auto lock_slot : slots_) {
|
|
locks_.emplace_back(owner_->mutex(lock_slot));
|
|
}
|
|
}
|
|
}
|
|
public:
|
|
template <std::size_t Index>
|
|
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
return owner_->template read_unlocked<Index>(*this);
|
|
}
|
|
template <auto Member>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return get_index<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return get_index<index>();
|
|
}
|
|
};
|
|
class Basic_Write_Guard {
|
|
Property_Object* owner_{};
|
|
std::vector<std::size_t> slots_;
|
|
std::vector<std::size_t> unsynchronized_properties_;
|
|
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::vector<std::unique_lock<mutex_type>>, Empty_Guard_Locks> locks_;
|
|
bool holds(std::size_t index) const {
|
|
auto lock_slot = owner_->slot(index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
return std::binary_search(unsynchronized_properties_.begin(), unsynchronized_properties_.end(), index);
|
|
}
|
|
return std::binary_search(slots_.begin(), slots_.end(), lock_slot);
|
|
}
|
|
friend class Property_Object;
|
|
Basic_Write_Guard(Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) {
|
|
if constexpr (uses_real_mutexes) {
|
|
locks_.reserve(slots_.size());
|
|
for (auto lock_slot : slots_) {
|
|
locks_.emplace_back(owner_->mutex(lock_slot));
|
|
}
|
|
}
|
|
}
|
|
public:
|
|
template <std::size_t Index>
|
|
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
return owner_->template read_unlocked<Index>(*this);
|
|
}
|
|
template <auto Member>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return get_index<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return get_index<index>();
|
|
}
|
|
template <std::size_t Index, class Value>
|
|
void set_index(Value&& value) requires Schema_Writable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
owner_->template write_unlocked<Index>(std::forward<Value>(value));
|
|
}
|
|
template <auto Member, class Value>
|
|
void set(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
set_index<index>(std::forward<Value>(value));
|
|
}
|
|
template <Fixed_String Key, class Value>
|
|
void set(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
set_index<index>(std::forward<Value>(value));
|
|
}
|
|
};
|
|
template <std::size_t Capacity>
|
|
class Static_Read_Guard {
|
|
const Property_Object* owner_{};
|
|
Static_Lock_Targets<Capacity> targets_;
|
|
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::array<std::shared_lock<mutex_type>, Capacity>, Empty_Guard_Locks> locks_{};
|
|
bool holds(std::size_t index) const {
|
|
auto lock_slot = owner_->slot(index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
return std::binary_search(targets_.unsynchronized_properties.begin(), targets_.unsynchronized_properties.begin() + static_cast<std::ptrdiff_t>(targets_.unsynchronized_count), index);
|
|
}
|
|
return std::binary_search(targets_.slots.begin(), targets_.slots.begin() + static_cast<std::ptrdiff_t>(targets_.slot_count), lock_slot);
|
|
}
|
|
friend class Property_Object;
|
|
Static_Read_Guard(const Property_Object& owner, Static_Lock_Targets<Capacity> targets) : owner_(&owner), targets_(std::move(targets)) {
|
|
if constexpr (uses_real_mutexes) {
|
|
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
|
|
locks_[index] = std::shared_lock<mutex_type>{owner_->mutex(targets_.slots[index])};
|
|
}
|
|
}
|
|
}
|
|
public:
|
|
template <std::size_t Index>
|
|
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
return owner_->template read_unlocked<Index>(*this);
|
|
}
|
|
template <auto Member>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return get_index<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return get_index<index>();
|
|
}
|
|
};
|
|
template <std::size_t Capacity>
|
|
class Static_Write_Guard {
|
|
Property_Object* owner_{};
|
|
Static_Lock_Targets<Capacity> targets_;
|
|
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::array<std::unique_lock<mutex_type>, Capacity>, Empty_Guard_Locks> locks_{};
|
|
bool holds(std::size_t index) const {
|
|
auto lock_slot = owner_->slot(index);
|
|
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
return std::binary_search(targets_.unsynchronized_properties.begin(), targets_.unsynchronized_properties.begin() + static_cast<std::ptrdiff_t>(targets_.unsynchronized_count), index);
|
|
}
|
|
return std::binary_search(targets_.slots.begin(), targets_.slots.begin() + static_cast<std::ptrdiff_t>(targets_.slot_count), lock_slot);
|
|
}
|
|
friend class Property_Object;
|
|
Static_Write_Guard(Property_Object& owner, Static_Lock_Targets<Capacity> targets) : owner_(&owner), targets_(std::move(targets)) {
|
|
if constexpr (uses_real_mutexes) {
|
|
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
|
|
locks_[index] = std::unique_lock<mutex_type>{owner_->mutex(targets_.slots[index])};
|
|
}
|
|
}
|
|
}
|
|
public:
|
|
template <std::size_t Index>
|
|
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
return owner_->template read_unlocked<Index>(*this);
|
|
}
|
|
template <auto Member>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return get_index<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return get_index<index>();
|
|
}
|
|
template <std::size_t Index, class Value>
|
|
void set_index(Value&& value) requires Schema_Writable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
|
|
if (!holds(Index)) {
|
|
throw std::logic_error("Property is outside the held synchronization set");
|
|
}
|
|
owner_->template write_unlocked<Index>(std::forward<Value>(value));
|
|
}
|
|
template <auto Member, class Value>
|
|
void set(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
set_index<index>(std::forward<Value>(value));
|
|
}
|
|
template <Fixed_String Key, class Value>
|
|
void set(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
set_index<index>(std::forward<Value>(value));
|
|
}
|
|
};
|
|
public:
|
|
using Read_Guard = Basic_Read_Guard;
|
|
using Write_Guard = Basic_Write_Guard;
|
|
private:
|
|
template <class Function>
|
|
void for_each_readable_impl(Function&& function) const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
const auto& schema = type_descriptor<Derived>();
|
|
schema.for_each_property([&](auto index, const auto& descriptor) {
|
|
constexpr std::size_t property_index = decltype(index)::value;
|
|
if constexpr (Schema::template property_type<property_index>::readable) {
|
|
auto value = read_one<property_index>();
|
|
std::invoke(function, index, descriptor, value);
|
|
}
|
|
});
|
|
}
|
|
template <class Function>
|
|
void for_each_readable_locked_impl(Function&& function) const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
auto targets = collect_all_static_lock_targets<true>();
|
|
Static_Read_Guard<Schema::property_count> guard{*this, std::move(targets)};
|
|
type_descriptor<Derived>().for_each_property([&](auto index, const auto& descriptor) {
|
|
constexpr std::size_t property_index = decltype(index)::value;
|
|
if constexpr (Schema::template property_type<property_index>::readable) {
|
|
std::invoke(function, index, descriptor, guard.template get_index<property_index>());
|
|
}
|
|
});
|
|
}
|
|
template <class Function>
|
|
void with_all_writable_locked_impl(Function&& function) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
auto targets = collect_all_static_lock_targets<false>();
|
|
Static_Write_Guard<Schema::property_count> guard{*this, std::move(targets)};
|
|
std::invoke(std::forward<Function>(function), guard);
|
|
}
|
|
static const std::type_info& runtime_object_type_impl() noexcept {
|
|
return typeid(Derived);
|
|
}
|
|
static std::size_t runtime_property_count_impl() noexcept {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
return Schema::property_count;
|
|
}
|
|
Runtime_Access_Result runtime_read_impl_local(std::string_view key, void* context, Runtime_Read_Callback callback) const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
auto index = schema_property_index<Schema>(key);
|
|
if (!index) {
|
|
return Runtime_Access_Result::unknown_property;
|
|
}
|
|
if (!runtime_property_readable(*index)) {
|
|
return Runtime_Access_Result::not_readable;
|
|
}
|
|
Runtime_Access_Result result = Runtime_Access_Result::unknown_property;
|
|
visit_schema_property_at(type_descriptor<Derived>(), *index, [&](auto property_index_constant, const auto&) {
|
|
constexpr std::size_t property_index = decltype(property_index_constant)::value;
|
|
using Property = typename Schema::template property_type<property_index>;
|
|
using Value = typename Property::value_type;
|
|
if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) {
|
|
if constexpr (std::is_reference_v<decltype(read_unlocked<property_index>(*this))>) {
|
|
auto&& value = read_unlocked<property_index>(*this);
|
|
callback(context, property_index, key, typeid(Value), std::addressof(value));
|
|
} else {
|
|
Value value = read_unlocked<property_index>(*this);
|
|
callback(context, property_index, key, typeid(Value), std::addressof(value));
|
|
}
|
|
} else {
|
|
auto lock_slot = slot(property_index);
|
|
Single_Read_View<property_index> view{*this, lock_slot};
|
|
auto emit = [&] {
|
|
if constexpr (std::is_reference_v<decltype(read_unlocked<property_index>(view))>) {
|
|
auto&& value = read_unlocked<property_index>(view);
|
|
callback(context, property_index, key, typeid(Value), std::addressof(value));
|
|
} else {
|
|
Value value = read_unlocked<property_index>(view);
|
|
callback(context, property_index, key, typeid(Value), std::addressof(value));
|
|
}
|
|
};
|
|
if constexpr (!uses_real_mutexes) {
|
|
emit();
|
|
} else if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
|
|
emit();
|
|
} else {
|
|
std::shared_lock lock{mutex(lock_slot)};
|
|
emit();
|
|
}
|
|
}
|
|
result = Runtime_Access_Result::ok;
|
|
});
|
|
return result;
|
|
}
|
|
static Runtime_Access_Result runtime_read_impl(const Property_Object_Base& base, std::string_view key, void* context, Runtime_Read_Callback callback) {
|
|
const auto& self = static_cast<const Property_Object&>(base);
|
|
return self.runtime_read_impl_local(key, context, callback);
|
|
}
|
|
Runtime_Access_Result runtime_write_impl_local(std::string_view key, const std::type_info& value_type, const void* value) {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
auto index = schema_property_index<Schema>(key);
|
|
if (!index) {
|
|
return Runtime_Access_Result::unknown_property;
|
|
}
|
|
if (!runtime_property_writable(*index)) {
|
|
return Runtime_Access_Result::not_writable;
|
|
}
|
|
Runtime_Access_Result result = Runtime_Access_Result::unknown_property;
|
|
visit_schema_property_at(type_descriptor<Derived>(), *index, [&](auto property_index_constant, const auto&) {
|
|
constexpr std::size_t property_index = decltype(property_index_constant)::value;
|
|
using Property = typename Schema::template property_type<property_index>;
|
|
using Value = typename Property::value_type;
|
|
if constexpr (!Property::runtime_copy_writable) {
|
|
result = Runtime_Access_Result::unsupported_runtime_write;
|
|
} else if (value_type != typeid(Value)) {
|
|
result = Runtime_Access_Result::type_mismatch;
|
|
} else {
|
|
write_one<property_index>(*static_cast<const Value*>(value));
|
|
result = Runtime_Access_Result::ok;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
static Runtime_Access_Result runtime_write_impl(Property_Object_Base& base, std::string_view key, const std::type_info& value_type, const void* value) {
|
|
auto& self = static_cast<Property_Object&>(base);
|
|
return self.runtime_write_impl_local(key, value_type, value);
|
|
}
|
|
static const Property_Object_Base::Runtime_Interface* runtime_interface() noexcept {
|
|
static const Property_Object_Base::Runtime_Interface value{
|
|
&runtime_object_type_impl,
|
|
&runtime_property_count_impl,
|
|
&runtime_read_impl,
|
|
&runtime_write_impl
|
|
};
|
|
return &value;
|
|
}
|
|
protected:
|
|
Property_Object() : Property_Object_Base(runtime_interface()), mutex_storage_(default_resolved_synchronization().lock_count) {}
|
|
explicit Property_Object(Property_Synchronization synchronization) : Property_Object_Base(runtime_interface()) {
|
|
initialize(type_descriptor<Derived>(), synchronization.plan);
|
|
}
|
|
Property_Object(const Property_Object& other) : Property_Object_Base(runtime_interface()) {
|
|
copy_synchronization_from(other);
|
|
}
|
|
Property_Object(Property_Object&& other) : Property_Object_Base(runtime_interface()) {
|
|
copy_synchronization_from(other);
|
|
}
|
|
Property_Object& operator=(const Property_Object&) noexcept {
|
|
return *this;
|
|
}
|
|
Property_Object& operator=(Property_Object&&) noexcept {
|
|
return *this;
|
|
}
|
|
~Property_Object() = default;
|
|
public:
|
|
const auto& schema() const noexcept {
|
|
return type_descriptor<Derived>();
|
|
}
|
|
/// Returns the effective lock topology after intrinsic capability filtering and any per-instance override.
|
|
Resolved_Synchronization_View resolved_synchronization() const noexcept {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
if (custom_lock_layout_) {
|
|
return {{custom_lock_layout_.get() + 1, Schema::property_count}, custom_lock_layout_[0]};
|
|
}
|
|
const auto& resolved = default_resolved_synchronization();
|
|
return {resolved.lock_slots, resolved.lock_count};
|
|
}
|
|
Derived& unsafe_object() noexcept {
|
|
return static_cast<Derived&>(*this);
|
|
}
|
|
const Derived& unsafe_object() const noexcept {
|
|
return static_cast<const Derived&>(*this);
|
|
}
|
|
template <auto Member>
|
|
auto read() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
return read_one<index>();
|
|
}
|
|
template <Fixed_String Key>
|
|
auto read() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
return read_one<index>();
|
|
}
|
|
template <auto Member, class Value>
|
|
void write(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
write_one<index>(std::forward<Value>(value));
|
|
}
|
|
template <Fixed_String Key, class Value>
|
|
void write(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_property_index_v<Schema, Key>;
|
|
write_one<index>(std::forward<Value>(value));
|
|
}
|
|
/// Returns the resolved lock slot for a registered member. `unsynchronized_slot` means no mutex is created for that property.
|
|
template <auto Member>
|
|
std::size_t lock_slot() const noexcept {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
constexpr auto index = schema_member_property_index_v<Schema, Member>;
|
|
static_assert(index < Schema::property_count);
|
|
return slot(index);
|
|
}
|
|
private:
|
|
Basic_Read_Guard lock_shared_impl(std::span<const std::string_view> keys) const {
|
|
return Basic_Read_Guard{*this, collect_dynamic_lock_targets(keys, true)};
|
|
}
|
|
Basic_Read_Guard lock_shared_impl(std::initializer_list<std::string_view> keys) const {
|
|
return lock_shared_impl(std::span<const std::string_view>{keys.begin(), keys.size()});
|
|
}
|
|
Basic_Write_Guard lock_unique_impl(std::span<const std::string_view> keys) {
|
|
return Basic_Write_Guard{*this, collect_dynamic_lock_targets(keys, false)};
|
|
}
|
|
Basic_Write_Guard lock_unique_impl(std::initializer_list<std::string_view> keys) {
|
|
return lock_unique_impl(std::span<const std::string_view>{keys.begin(), keys.size()});
|
|
}
|
|
template <auto... Members>
|
|
auto lock_shared_impl() const {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static_assert((Schema_Property_Member<Schema, Members> && ...));
|
|
auto targets = collect_static_lock_targets<true, schema_member_property_index_v<Schema, Members>...>();
|
|
return Static_Read_Guard<sizeof...(Members)>{*this, std::move(targets)};
|
|
}
|
|
template <auto... Members>
|
|
auto lock_unique_impl() {
|
|
using Schema = type_descriptor_schema_t<Derived>;
|
|
static_assert((Schema_Property_Member<Schema, Members> && ...));
|
|
auto targets = collect_static_lock_targets<false, schema_member_property_index_v<Schema, Members>...>();
|
|
return Static_Write_Guard<sizeof...(Members)>{*this, std::move(targets)};
|
|
}
|
|
public:
|
|
/// Runtime-key shared guard. Keys are validated at runtime; unknown or non-readable properties throw `std::invalid_argument`. Lock domains are deduplicated and acquired in stable slot order.
|
|
Read_Guard lock_shared(std::span<const std::string_view> keys) const {
|
|
return lock_shared_impl(keys);
|
|
}
|
|
Read_Guard lock_shared(std::initializer_list<std::string_view> keys) const {
|
|
return lock_shared_impl(keys);
|
|
}
|
|
/// Runtime-key unique guard. Keys are validated at runtime; unknown or non-writable properties throw `std::invalid_argument`. Lock domains are deduplicated and acquired in stable slot order.
|
|
Write_Guard lock_unique(std::span<const std::string_view> keys) {
|
|
return lock_unique_impl(keys);
|
|
}
|
|
Write_Guard lock_unique(std::initializer_list<std::string_view> keys) {
|
|
return lock_unique_impl(keys);
|
|
}
|
|
/// Compile-time shared guard for registered readable members. Invalid or non-readable selections are removed by constraints before the function can be called.
|
|
template <auto... Members>
|
|
auto lock_shared() const requires (Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Members> && ...) {
|
|
return lock_shared_impl<Members...>();
|
|
}
|
|
/// Compile-time unique guard for registered writable members. The implementation sorts and deduplicates resolved slots so callers may request members in any order without introducing lock-order inversion.
|
|
template <auto... Members>
|
|
auto lock_unique() requires (Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Members> && ...) {
|
|
return lock_unique_impl<Members...>();
|
|
}
|
|
template <class Function>
|
|
void for_each_readable(Function&& function) const {
|
|
for_each_readable_impl(std::forward<Function>(function));
|
|
}
|
|
template <class Function>
|
|
void for_each_readable_locked(Function&& function) const {
|
|
for_each_readable_locked_impl(std::forward<Function>(function));
|
|
}
|
|
template <class Function>
|
|
void with_all_writable_locked(Function&& function) {
|
|
with_all_writable_locked_impl(std::forward<Function>(function));
|
|
}
|
|
};
|
|
}
|