去除所有访问级别
This commit is contained in:
+276
-355
@@ -2,227 +2,235 @@
|
||||
|
||||
[中文](CORE_GUIDE.zh-CN.md)
|
||||
|
||||
## 1. Include and target
|
||||
|
||||
Include the complete Core surface with:
|
||||
## 1. Include and CMake target
|
||||
|
||||
```cpp
|
||||
#include <structive/property/property.hpp>
|
||||
```
|
||||
|
||||
CMake target:
|
||||
|
||||
```cmake
|
||||
target_link_libraries(my_target PRIVATE structive::property_core)
|
||||
```
|
||||
|
||||
The Core target is header-only and requires C++20.
|
||||
Property Core is C++20.
|
||||
|
||||
## 2. Define a managed object
|
||||
|
||||
A managed object normally derives from `Property_Object<Derived>`:
|
||||
|
||||
```cpp
|
||||
using namespace structive;
|
||||
struct Device : Property_Object<Device> {
|
||||
double temperature{25.0};
|
||||
double pressure{101.3};
|
||||
double min_speed{10.0};
|
||||
double max_speed{100.0};
|
||||
std::string name{"device-1"};
|
||||
int serial_number{1001};
|
||||
};
|
||||
```
|
||||
|
||||
The members remain ordinary C++ members.
|
||||
`Property_Object<Device>` adds managed operations. The fields remain ordinary members.
|
||||
|
||||
## 3. Define the type descriptor
|
||||
|
||||
Specialize `Type_Descriptor<T>` and return an `Object_Schema` through `object<T>(...)`:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
struct structive::Type_Descriptor<Device> {
|
||||
static auto get() {
|
||||
return object<Device>(
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
),
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
),
|
||||
synchronization(sync_all_independent),
|
||||
field<&Device::temperature>(
|
||||
key<"temperature">,
|
||||
unit<"C">,
|
||||
min_value<-50.0>,
|
||||
max_value<200.0>
|
||||
),
|
||||
field<&Device::pressure>(key<"pressure">, unit<"kPa">),
|
||||
field<&Device::min_speed>(key<"min_speed">),
|
||||
field<&Device::max_speed>(key<"max_speed">),
|
||||
field<&Device::name>(key<"name">)
|
||||
field<&Device::pressure>(
|
||||
key<"pressure">,
|
||||
unit<"kPa">
|
||||
),
|
||||
field<&Device::serial_number>(
|
||||
key<"serial_number">,
|
||||
read_only
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`field<Member>(...)` is an alias of `property<Member>(...)` and produces a member-backed `Property_Descriptor`.
|
||||
The descriptor is the structural definition of `Device`.
|
||||
|
||||
## 4. Schema guarantees
|
||||
|
||||
The schema enforces several structural conditions:
|
||||
A valid schema guarantees:
|
||||
|
||||
- every property has a non-empty `key`;
|
||||
- property keys are unique;
|
||||
- the same member storage identity cannot be registered twice;
|
||||
- single-valued Attribute categories cannot appear more than once on the same declaration;
|
||||
- constraints must be compatible with the property value type;
|
||||
- object defaults may contain only inheritable Attributes;
|
||||
- external/persistence capabilities cannot claim operations unsupported by the accessor.
|
||||
- every registered property has a non-empty key;
|
||||
- keys are unique;
|
||||
- member selectors belong to the described object type;
|
||||
- single-valued Attribute categories are not duplicated on the same property;
|
||||
- capability metadata cannot request operations unsupported by the underlying accessor;
|
||||
- constraints are compatible with the property value type.
|
||||
|
||||
Many schema errors are therefore compile-time errors.
|
||||
Use:
|
||||
|
||||
```cpp
|
||||
static_assert(Property_Described_Object<Device>);
|
||||
using Schema = type_descriptor_schema_t<Device>;
|
||||
static_assert(Valid_Property_Schema<Schema>);
|
||||
```
|
||||
|
||||
## 5. Access the schema
|
||||
|
||||
For a described type:
|
||||
|
||||
```cpp
|
||||
const auto& schema = type_descriptor<Device>();
|
||||
const auto& same_schema = device.schema();
|
||||
```
|
||||
|
||||
For an instance:
|
||||
|
||||
```cpp
|
||||
Device device;
|
||||
const auto& schema = device.schema();
|
||||
```
|
||||
|
||||
Property lookup supports a numeric compile-time index or a member pointer:
|
||||
Properties can be selected by index or member pointer:
|
||||
|
||||
```cpp
|
||||
const auto& first = schema.property<0>();
|
||||
const auto& temperature = schema.property<&Device::temperature>();
|
||||
```
|
||||
|
||||
Use member pointers in typed business code. Use indexes mainly in generic traversal.
|
||||
|
||||
## 6. Property descriptor information
|
||||
|
||||
A property descriptor exposes:
|
||||
A property descriptor exposes compile-time structural facts:
|
||||
|
||||
```cpp
|
||||
auto key_value = temperature.key();
|
||||
```
|
||||
|
||||
and compile-time traits such as:
|
||||
|
||||
```cpp
|
||||
using Property = std::remove_cvref_t<decltype(temperature)>;
|
||||
using Property = std::remove_cvref_t<decltype(schema.property<&Device::temperature>())>;
|
||||
static_assert(Property::readable);
|
||||
static_assert(Property::writable);
|
||||
using Value = Property::value_type;
|
||||
using Accessor = Property::accessor_type;
|
||||
```
|
||||
|
||||
Category-based Attribute lookup is available through:
|
||||
A `read_only` field reports:
|
||||
|
||||
```cpp
|
||||
static_assert(Property::has_attribute<Unit_Category>);
|
||||
const auto& attribute = temperature.attribute<Unit_Category>();
|
||||
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
|
||||
static_assert(Serial::readable);
|
||||
static_assert(!Serial::writable);
|
||||
```
|
||||
|
||||
All declared Attributes can be traversed:
|
||||
The key is available at runtime without dynamic allocation:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_attribute([](const auto& attribute) {
|
||||
// inspect attribute type/value
|
||||
});
|
||||
auto key_value = schema.property<&Device::temperature>().key();
|
||||
```
|
||||
|
||||
Constraints can be traversed separately:
|
||||
## 7. Intrinsic capability
|
||||
|
||||
Core capability values are:
|
||||
|
||||
```cpp
|
||||
temperature.for_each_constraint([](const auto& constraint_value) {
|
||||
// inspect or evaluate a constraint
|
||||
});
|
||||
Property_Capability::none
|
||||
Property_Capability::read
|
||||
Property_Capability::write
|
||||
Property_Capability::read_write
|
||||
```
|
||||
|
||||
## 7. Object defaults and effective Attributes
|
||||
|
||||
`defaults(...)` provides object-wide values for inheritable Attribute categories:
|
||||
Convenience Attributes are:
|
||||
|
||||
```cpp
|
||||
defaults(
|
||||
external_access<External_Access::read_write>,
|
||||
persistence_access<Persistence_Access::load_store>,
|
||||
sensitive<false>
|
||||
)
|
||||
read_only
|
||||
write_only
|
||||
read_write
|
||||
inaccessible
|
||||
```
|
||||
|
||||
A property may override a default:
|
||||
The capability belongs to the property itself. It is not an authorization rule.
|
||||
|
||||
```cpp
|
||||
field<&Device::name>(
|
||||
key<"name">,
|
||||
external_access<External_Access::read>
|
||||
)
|
||||
If capability metadata is absent, Structive derives capability from the accessor.
|
||||
|
||||
For a normal non-const member field, the default is read/write.
|
||||
|
||||
For a getter-only computed property, the default is read-only.
|
||||
|
||||
Capability metadata can narrow the accessor, but it cannot create an operation the accessor does not support.
|
||||
|
||||
## 8. No access-control API
|
||||
|
||||
Property Core intentionally does not provide access modes or domain views.
|
||||
|
||||
There is no built-in distinction between:
|
||||
|
||||
```text
|
||||
internal
|
||||
external
|
||||
persistence
|
||||
```
|
||||
|
||||
Core exposes effective access traits such as `effective_external_access_v`, `external_readable_v`, `external_writable_v`, `persistence_loadable_v`, `persistence_storable_v` and `effective_sensitive_v` for generic code.
|
||||
A consumer decides its own policy. For example, a GUI may choose to expose only selected properties even though all of them are structurally readable.
|
||||
|
||||
Extensions can use `declared_effective_attribute<Index, Category>(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category.
|
||||
Core only exposes intrinsic `readable` and `writable` facts.
|
||||
|
||||
## 8. Core Attributes
|
||||
## 9. Core Attributes
|
||||
|
||||
### 8.1 Key
|
||||
### 9.1 Key
|
||||
|
||||
Every property requires one key:
|
||||
|
||||
```cpp
|
||||
key<"temperature">
|
||||
```
|
||||
|
||||
Required for every property. Non-empty and unique per schema.
|
||||
The key is the structural protocol identifier used by runtime lookup and adapters.
|
||||
|
||||
### 8.2 External access
|
||||
### 9.2 Capability
|
||||
|
||||
```cpp
|
||||
external_access<External_Access::none>
|
||||
external_access<External_Access::read>
|
||||
external_access<External_Access::write>
|
||||
external_access<External_Access::read_write>
|
||||
read_only
|
||||
write_only
|
||||
read_write
|
||||
inaccessible
|
||||
```
|
||||
|
||||
This Attribute is inheritable through `defaults(...)`.
|
||||
Capability Attributes are single-valued and non-inheritable.
|
||||
|
||||
### 8.3 Persistence access
|
||||
|
||||
```cpp
|
||||
persistence_access<Persistence_Access::none>
|
||||
persistence_access<Persistence_Access::load>
|
||||
persistence_access<Persistence_Access::store>
|
||||
persistence_access<Persistence_Access::load_store>
|
||||
```
|
||||
|
||||
This Attribute is also inheritable.
|
||||
|
||||
### 8.4 Unit
|
||||
### 9.3 Unit
|
||||
|
||||
```cpp
|
||||
unit<"C">
|
||||
unit<"kPa">
|
||||
```
|
||||
|
||||
This is descriptive metadata and is not inheritable.
|
||||
Core stores unit metadata but does not perform conversion.
|
||||
|
||||
### 8.5 Sensitive
|
||||
### 9.4 Sensitive
|
||||
|
||||
```cpp
|
||||
sensitive<>
|
||||
sensitive<false>
|
||||
```
|
||||
|
||||
This is inheritable metadata. Core exposes its effective value but does not automatically redact data.
|
||||
`sensitive` is inheritable metadata. It does not implement authorization. A consumer may use it as one input to its own policy.
|
||||
|
||||
## 9. Constraints and validation
|
||||
## 10. Custom Attributes and defaults
|
||||
|
||||
Built-in constraints:
|
||||
Any type following the Attribute protocol can be attached to a property.
|
||||
|
||||
```cpp
|
||||
struct Group_Category {};
|
||||
template <Fixed_String Value>
|
||||
struct Group_Attribute {
|
||||
using attribute_category = Group_Category;
|
||||
static constexpr bool single_valued = true;
|
||||
static constexpr bool inheritable = true;
|
||||
static constexpr auto value = Value;
|
||||
};
|
||||
```
|
||||
|
||||
Inheritable Attributes can be supplied by `defaults(...)`:
|
||||
|
||||
```cpp
|
||||
object<Device>(
|
||||
defaults(sensitive<>),
|
||||
field<&Device::temperature>(key<"temperature">)
|
||||
)
|
||||
```
|
||||
|
||||
Capability is intentionally non-inheritable because each property owns its intrinsic operation set.
|
||||
|
||||
## 11. Constraints and validation
|
||||
|
||||
Built-in constraints include:
|
||||
|
||||
```cpp
|
||||
min_value<0>
|
||||
@@ -230,7 +238,7 @@ max_value<100>
|
||||
finite
|
||||
```
|
||||
|
||||
Custom constraint:
|
||||
Custom constraints:
|
||||
|
||||
```cpp
|
||||
constraint<"even">([](int value) {
|
||||
@@ -238,291 +246,241 @@ constraint<"even">([](int value) {
|
||||
})
|
||||
```
|
||||
|
||||
Validation by member pointer:
|
||||
Validation is explicit:
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||||
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||||
if (result) {
|
||||
auto key_value = result->property_key;
|
||||
auto code = result->code;
|
||||
}
|
||||
```
|
||||
|
||||
Validation by compile-time key:
|
||||
`write()` does not automatically call validation.
|
||||
|
||||
```cpp
|
||||
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
|
||||
```
|
||||
## 12. Typed managed read/write
|
||||
|
||||
A failure returns:
|
||||
|
||||
```cpp
|
||||
struct Validation_Error {
|
||||
std::string_view property_key;
|
||||
std::string_view code;
|
||||
};
|
||||
```
|
||||
|
||||
Validation is explicit and is not automatically executed by managed writes.
|
||||
|
||||
## 10. Intrinsic managed read/write
|
||||
|
||||
Typed member-pointer access:
|
||||
Member-pointer access is the preferred C++ API:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read<&Device::temperature>();
|
||||
device.write<&Device::temperature>(30.0);
|
||||
```
|
||||
|
||||
Compile-time key access:
|
||||
Compile-time key access is also available:
|
||||
|
||||
```cpp
|
||||
auto temperature = device.read_key<"temperature">();
|
||||
device.write_key<"temperature">(30.0);
|
||||
```
|
||||
|
||||
These operations use the property's intrinsic capability directly. There is no separate `internal` capability mode; readability and writability come from the accessor itself.
|
||||
|
||||
`read()` returns a value object, not a reference to storage. The read is performed while the configured shared lock is held when synchronization is enabled.
|
||||
|
||||
## 11. External capability view
|
||||
|
||||
Object-level defaults may expose properties externally:
|
||||
The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution.
|
||||
|
||||
```cpp
|
||||
defaults(external_access<External_Access::read_write>)
|
||||
template <class Object>
|
||||
concept Can_Write_Serial = requires(Object& object) {
|
||||
object.template write<&Device::serial_number>(1);
|
||||
};
|
||||
static_assert(!Can_Write_Serial<Device>);
|
||||
```
|
||||
|
||||
Use:
|
||||
## 13. Read-only fast path
|
||||
|
||||
Stored read-only properties are statically removed from managed locking.
|
||||
|
||||
Given:
|
||||
|
||||
```cpp
|
||||
device.external().write<&Device::temperature>(30.0);
|
||||
auto value = device.external().read<&Device::temperature>();
|
||||
field<&Device::serial_number>(key<"serial_number">, read_only)
|
||||
```
|
||||
|
||||
A const object produces a const capability view and therefore has no write API.
|
||||
Structive resolves:
|
||||
|
||||
The typed API rejects statically inaccessible operations at compile time.
|
||||
|
||||
## 12. Persistence capability view
|
||||
|
||||
Use persistence terminology instead of generic write/read:
|
||||
|
||||
```cpp
|
||||
device.persistence().load<&Device::temperature>(30.0);
|
||||
auto value = device.persistence().store<&Device::temperature>();
|
||||
```text
|
||||
serial_number -> unsynchronized_slot
|
||||
```
|
||||
|
||||
Compile-time key forms are also available:
|
||||
and `read<&Device::serial_number>()` does not query a lock slot or construct a `shared_lock`.
|
||||
|
||||
```cpp
|
||||
device.persistence().load_key<"temperature">(30.0);
|
||||
auto value = device.persistence().store_key<"temperature">();
|
||||
```
|
||||
This remains true even when the broad default is `sync_all_shared`.
|
||||
|
||||
The persistence view is controlled by `Persistence_Access` metadata.
|
||||
If an object has only stored read-only properties, those properties contribute zero mutexes to `resolved_synchronization().lock_count`.
|
||||
|
||||
## 13. Synchronization plans
|
||||
## 14. Synchronization plans
|
||||
|
||||
### 13.1 Default independent
|
||||
|
||||
The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain.
|
||||
|
||||
Explicit form:
|
||||
### 14.1 Independent
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_independent)
|
||||
```
|
||||
|
||||
### 13.2 Shared
|
||||
Each property that actually requires synchronization gets its own lock domain.
|
||||
|
||||
### 14.2 Shared
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_shared)
|
||||
```
|
||||
|
||||
All properties use the same lock slot unless overridden.
|
||||
All properties that require synchronization share one lock domain.
|
||||
|
||||
### 13.3 Unsynchronized
|
||||
Read-only stored properties are excluded before lock slots are materialized.
|
||||
|
||||
### 14.3 Unsynchronized
|
||||
|
||||
```cpp
|
||||
synchronization(sync_all_unsynchronized)
|
||||
```
|
||||
|
||||
Properties use the `unsynchronized_slot` and no real Structive mutex protects them.
|
||||
Managed access performs no real locking even for writable properties.
|
||||
|
||||
### 13.4 Per-property override
|
||||
|
||||
By member pointer:
|
||||
### 14.4 Per-property override
|
||||
|
||||
```cpp
|
||||
sync_independent<&Device::temperature>()
|
||||
sync_unsynchronized<&Device::immutable_id>()
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_unsynchronized<&Device::temperature>()
|
||||
)
|
||||
```
|
||||
|
||||
Runtime string forms also exist:
|
||||
Dynamic-key rule forms are also available.
|
||||
|
||||
### 14.5 Groups
|
||||
|
||||
```cpp
|
||||
sync_independent("temperature")
|
||||
sync_unsynchronized("immutable_id")
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
)
|
||||
```
|
||||
|
||||
Prefer member-pointer rules when the property is statically known.
|
||||
Members in one group resolve to the same lock slot if they require synchronization.
|
||||
|
||||
### 13.5 Groups
|
||||
## 15. Per-instance synchronization override
|
||||
|
||||
Member-pointer form:
|
||||
An object can explicitly override the type default:
|
||||
|
||||
```cpp
|
||||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||||
```
|
||||
|
||||
String form:
|
||||
|
||||
```cpp
|
||||
sync_group("speed_range", "min_speed", "max_speed")
|
||||
```
|
||||
|
||||
All properties in a group share one lock slot.
|
||||
|
||||
The resolver rejects unknown runtime keys, duplicate group names, empty groups and multiple explicit configurations of the same property.
|
||||
|
||||
## 14. Per-instance synchronization override
|
||||
|
||||
The type descriptor supplies a default synchronization plan, but an instance may override it through `Property_Synchronization`:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
Device() = default;
|
||||
explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
|
||||
};
|
||||
Device shared_device{
|
||||
property_synchronization(synchronization(sync_all_shared))
|
||||
};
|
||||
```
|
||||
|
||||
For schema-aware compile-time synchronization specifications:
|
||||
|
||||
```cpp
|
||||
auto policy = property_synchronization<Device>(
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::temperature, &Device::pressure>("environment")
|
||||
Device device{
|
||||
property_synchronization(
|
||||
synchronization(sync_all_shared)
|
||||
)
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
The schema remains the same; only that instance uses an override lock topology. Default instances share one resolved topology per type, so the default plan is not re-resolved for every object.
|
||||
For typed member rules:
|
||||
|
||||
## 15. Inspect resolved synchronization
|
||||
```cpp
|
||||
Device device{
|
||||
property_synchronization<Device>(
|
||||
synchronization(
|
||||
sync_all_independent,
|
||||
sync_group<&Device::temperature, &Device::pressure>("environment")
|
||||
)
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
For a typed member:
|
||||
The default topology is shared per type. Only an object with an explicit override stores its compact override layout.
|
||||
|
||||
## 16. Inspect resolved synchronization
|
||||
|
||||
```cpp
|
||||
auto view = device.resolved_synchronization();
|
||||
auto count = view.lock_count;
|
||||
```
|
||||
|
||||
Inspect a member slot:
|
||||
|
||||
```cpp
|
||||
auto slot = device.lock_slot<&Device::temperature>();
|
||||
```
|
||||
|
||||
For the complete view:
|
||||
No-lock properties use:
|
||||
|
||||
```cpp
|
||||
auto resolved = device.resolved_synchronization();
|
||||
auto count = resolved.lock_count;
|
||||
auto slot = resolved.slot(0);
|
||||
auto uses_lock = resolved.uses_lock(0);
|
||||
Resolved_Synchronization_View::unsynchronized_slot
|
||||
```
|
||||
|
||||
`Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties.
|
||||
For a read-only stored property this is automatic.
|
||||
|
||||
## 16. Multi-property static guards
|
||||
## 17. Static multi-property guards
|
||||
|
||||
Shared guard:
|
||||
Read guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
|
||||
auto temperature = guard.get<&Device::temperature>();
|
||||
auto pressure = guard.get<&Device::pressure>();
|
||||
```
|
||||
|
||||
Unique guard:
|
||||
Write guard:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
||||
auto old_min = guard.get<&Device::min_speed>();
|
||||
guard.set<&Device::min_speed>(20.0);
|
||||
guard.set<&Device::max_speed>(120.0);
|
||||
guard.set<&Device::min_speed>(20);
|
||||
guard.set<&Device::max_speed>(120);
|
||||
```
|
||||
|
||||
The guard only permits access to properties inside its held synchronization set.
|
||||
Static guards deduplicate slots and acquire them in stable slot order.
|
||||
|
||||
Capability views provide equivalent static guards constrained by their access mode:
|
||||
A typed unique guard requires all selected properties to be intrinsically writable.
|
||||
|
||||
## 18. Dynamic-key guards
|
||||
|
||||
```cpp
|
||||
auto guard = device.external().lock_shared<&Device::temperature>();
|
||||
auto read_guard = device.lock_shared({"temperature", "pressure"});
|
||||
auto write_guard = device.lock_unique({"temperature", "pressure"});
|
||||
```
|
||||
|
||||
## 17. Dynamic-key guards
|
||||
Unknown keys throw `std::invalid_argument`.
|
||||
|
||||
When a property set is known only at runtime:
|
||||
A dynamic unique guard rejects a read-only property because the key is only known at runtime.
|
||||
|
||||
## 19. Traversal
|
||||
|
||||
Schema-only traversal:
|
||||
|
||||
```cpp
|
||||
std::array<std::string_view, 2> keys{"temperature", "pressure"};
|
||||
auto guard = device.lock_shared(keys);
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```cpp
|
||||
auto guard = device.lock_unique({"min_speed", "max_speed"});
|
||||
```
|
||||
|
||||
Dynamic guards validate keys and capability visibility at runtime. Lock slots are sorted and deduplicated before acquisition.
|
||||
|
||||
Read/write through a dynamic guard can still use compile-time members/keys after the guard is acquired; access outside the held set throws `std::logic_error`.
|
||||
|
||||
## 18. Traversal
|
||||
|
||||
Schema traversal:
|
||||
|
||||
```cpp
|
||||
schema.for_each_property([](auto index, const auto& property) {
|
||||
// compile-time index and descriptor
|
||||
schema.for_each_property([&](auto index, const auto& descriptor) {
|
||||
});
|
||||
```
|
||||
|
||||
Managed value traversal:
|
||||
Readable value traversal:
|
||||
|
||||
```cpp
|
||||
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
|
||||
// one managed read per property
|
||||
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
|
||||
});
|
||||
```
|
||||
|
||||
Locked traversal acquires the complete readable synchronization set first:
|
||||
Locked readable traversal:
|
||||
|
||||
```cpp
|
||||
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
|
||||
// all selected readable properties are held under the guard
|
||||
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
|
||||
});
|
||||
```
|
||||
|
||||
For coordinated writable access:
|
||||
The locked traversal acquires only actual synchronization slots. Read-only stored properties do not introduce locks.
|
||||
|
||||
Writable transaction-style guard acquisition:
|
||||
|
||||
```cpp
|
||||
device.with_all_writable_locked([](auto& guard) {
|
||||
// use guard.get / guard.set
|
||||
device.with_all_writable_locked([&](auto& guard) {
|
||||
});
|
||||
```
|
||||
|
||||
The persistence view provides `with_all_loadable_locked(...)`.
|
||||
This acquires all writable managed lock domains; it does not perform validation or rollback automatically.
|
||||
|
||||
## 19. Computed property
|
||||
## 20. Computed properties
|
||||
|
||||
A synchronized computed property receives a read view:
|
||||
|
||||
```cpp
|
||||
computed_property<Device, double>([](const auto& view) {
|
||||
computed_property<Device, int>([](const auto& view) {
|
||||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||||
}, key<"speed_span">, external_access<External_Access::read>)
|
||||
}, key<"speed_span">)
|
||||
```
|
||||
|
||||
The computed property itself and every dependency read through the view must resolve to the same lock slot.
|
||||
|
||||
For example:
|
||||
Writable dependencies that must form one snapshot should share the computed synchronization domain:
|
||||
|
||||
```cpp
|
||||
synchronization(
|
||||
@@ -531,27 +489,27 @@ synchronization(
|
||||
)
|
||||
```
|
||||
|
||||
Computed properties are read-only.
|
||||
A read-only stored dependency may be read through the computed view without joining a lock slot, because it has no managed writer.
|
||||
|
||||
## 20. Trusted accessor properties
|
||||
## 21. Trusted accessor properties
|
||||
|
||||
Core also supports member-function-based trusted access:
|
||||
Trusted getter:
|
||||
|
||||
```cpp
|
||||
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
|
||||
trusted_computed_property<&Device::value>(key<"value">)
|
||||
```
|
||||
|
||||
and getter/setter pairs:
|
||||
Trusted getter/setter:
|
||||
|
||||
```cpp
|
||||
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
|
||||
trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
|
||||
```
|
||||
|
||||
These accessors invoke the object member functions directly and mark themselves as trusted object access. They do not use the synchronized computed-view dependency restriction. Use them when the member functions themselves define the correct synchronization/consistency contract.
|
||||
These forms access the object directly rather than using synchronized dependency views. They should only be used when the caller understands the synchronization contract of those member functions.
|
||||
|
||||
## 21. Runtime type-erased access
|
||||
## 22. Runtime type-erased access
|
||||
|
||||
Any `Property_Object<T>` is also a `Property_Object_Base`:
|
||||
Use `Property_Object_Base` for dynamic adapters:
|
||||
|
||||
```cpp
|
||||
Property_Object_Base& erased = device;
|
||||
@@ -564,112 +522,75 @@ erased.runtime_object_type();
|
||||
erased.runtime_property_count();
|
||||
```
|
||||
|
||||
Intrinsic runtime write does not require a mode:
|
||||
Read:
|
||||
|
||||
```cpp
|
||||
double value = 35.0;
|
||||
auto result = erased.runtime_write(
|
||||
"temperature",
|
||||
typeid(double),
|
||||
&value
|
||||
);
|
||||
auto result = erased.runtime_read("temperature", context, callback);
|
||||
```
|
||||
|
||||
A boundary projection is selected explicitly when an adapter must respect external or persistence capability metadata:
|
||||
Write:
|
||||
|
||||
```cpp
|
||||
auto external_result = erased.runtime_write(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
typeid(double),
|
||||
&value
|
||||
);
|
||||
```
|
||||
|
||||
Runtime read uses the same overload model and returns the value through a callback:
|
||||
|
||||
```cpp
|
||||
static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) {
|
||||
if (type == typeid(double)) {
|
||||
*static_cast<double*>(context) = *static_cast<const double*>(value);
|
||||
}
|
||||
}
|
||||
double output = 0.0;
|
||||
auto result = erased.runtime_read(
|
||||
"temperature",
|
||||
&output,
|
||||
&read_double
|
||||
);
|
||||
auto external_result = erased.runtime_read(
|
||||
Managed_Access_Mode::external,
|
||||
"temperature",
|
||||
&output,
|
||||
&read_double
|
||||
);
|
||||
auto result = erased.runtime_write("temperature", typeid(double), &value);
|
||||
```
|
||||
|
||||
Possible results:
|
||||
|
||||
```cpp
|
||||
Runtime_Access_Result::ok
|
||||
Runtime_Access_Result::unknown_property
|
||||
Runtime_Access_Result::not_readable
|
||||
Runtime_Access_Result::not_writable
|
||||
Runtime_Access_Result::type_mismatch
|
||||
```text
|
||||
ok
|
||||
unknown_property
|
||||
not_readable
|
||||
not_writable
|
||||
type_mismatch
|
||||
```
|
||||
|
||||
The intrinsic runtime overload uses the accessor's intrinsic capability and the same synchronization topology as typed `read()`/`write()`. The overload taking `Managed_Access_Mode` applies the selected external or persistence projection before using the same synchronization rules.
|
||||
Runtime access has no external/persistence mode. An adapter owns its own policy and chooses whether it calls the intrinsic read/write operation.
|
||||
|
||||
## 22. Lock policy
|
||||
Runtime read of a stored read-only property follows the same no-lock fast path as typed read.
|
||||
|
||||
## 23. Lock policies
|
||||
|
||||
Default:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
|
||||
};
|
||||
Property_Object<Device, Shared_Mutex_Policy>
|
||||
```
|
||||
|
||||
Equivalent shorthand:
|
||||
No-lock:
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device> {
|
||||
};
|
||||
Property_Object<Device, No_Lock_Policy>
|
||||
```
|
||||
|
||||
A no-op lock policy exists:
|
||||
`No_Lock_Policy` stores no real mutex array and avoids real lock objects on managed hot paths.
|
||||
|
||||
```cpp
|
||||
struct Device : Property_Object<Device, No_Lock_Policy> {
|
||||
};
|
||||
```
|
||||
|
||||
`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. Its default managed-object path stores no mutex array and performs no per-instance heap allocation for synchronization. A custom `Property_Synchronization` may still allocate a compact override topology because computed-property synchronization-domain checks must preserve the selected per-instance layout.
|
||||
|
||||
## 23. Raw object access
|
||||
|
||||
`unsafe_object()` exposes the derived object directly:
|
||||
## 24. Raw object access
|
||||
|
||||
```cpp
|
||||
Device& raw = device.unsafe_object();
|
||||
```
|
||||
|
||||
Direct field access is also normal C++:
|
||||
or normal public member access:
|
||||
|
||||
```cpp
|
||||
device.temperature = 40.0;
|
||||
device.temperature = 30.0;
|
||||
```
|
||||
|
||||
These paths intentionally bypass Structive-managed access. They are useful when the caller already owns the required synchronization or when a field is being handled outside Structive’s managed contract.
|
||||
Both bypass the Structive managed contract.
|
||||
|
||||
## 24. Recommended usage rules
|
||||
This is intentional. Structive is cooperative structural infrastructure, not forced encapsulation.
|
||||
|
||||
- Use member pointers for typed business access.
|
||||
- Use string keys at runtime integration boundaries.
|
||||
- Treat keys as protocol identifiers.
|
||||
- Keep object-wide access policy in `defaults(...)` and override only exceptions.
|
||||
- Use synchronization groups for fields that must be observed consistently.
|
||||
- Validate explicitly at the business operation boundary.
|
||||
- Use static multi-property guards for cross-field invariants.
|
||||
- Prefer synchronized computed properties over trusted accessors.
|
||||
- Use runtime access for adapters, not ordinary typed code.
|
||||
- Use raw access only when the caller intentionally owns the missing managed guarantees.
|
||||
## 25. Recommended usage rules
|
||||
|
||||
Use these defaults:
|
||||
|
||||
1. Use ordinary C++ members for storage.
|
||||
2. Register only fields that belong to the structural model.
|
||||
3. Prefer member-pointer typed APIs in C++ business code.
|
||||
4. Use `read_only` when the Structive managed model must never write a stored property.
|
||||
5. Rely on the resulting zero-lock optimization for stored read-only data.
|
||||
6. Keep raw writes to read-only properties outside concurrent managed code.
|
||||
7. Use synchronization groups for mutable cross-field consistency.
|
||||
8. Keep validation explicit.
|
||||
9. Use runtime access only for genuinely dynamic adapters.
|
||||
10. Let GUI/RPC/persistence/authorization layers own their own exposure and access policy.
|
||||
|
||||
Reference in New Issue
Block a user