661 lines
16 KiB
Markdown
661 lines
16 KiB
Markdown
# Structive Property Core Guide
|
||
|
||
[中文](CORE_GUIDE.zh-CN.md)
|
||
|
||
## 1. Include and target
|
||
|
||
Include the complete Core surface with:
|
||
|
||
```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.
|
||
|
||
## 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"};
|
||
};
|
||
```
|
||
|
||
The members remain ordinary C++ 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")
|
||
),
|
||
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<Member>(...)` is an alias of `property<Member>(...)` and produces a member-backed `Property_Descriptor`.
|
||
|
||
## 4. Schema guarantees
|
||
|
||
The schema enforces several structural conditions:
|
||
|
||
- 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.
|
||
|
||
Many schema errors are therefore compile-time errors.
|
||
|
||
## 5. Access the schema
|
||
|
||
For a described type:
|
||
|
||
```cpp
|
||
const auto& schema = type_descriptor<Device>();
|
||
```
|
||
|
||
For an instance:
|
||
|
||
```cpp
|
||
Device device;
|
||
const auto& schema = device.schema();
|
||
```
|
||
|
||
Property lookup supports a numeric compile-time index or a 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:
|
||
|
||
```cpp
|
||
auto key_value = temperature.key();
|
||
```
|
||
|
||
and compile-time traits such as:
|
||
|
||
```cpp
|
||
using Property = std::remove_cvref_t<decltype(temperature)>;
|
||
static_assert(Property::readable);
|
||
static_assert(Property::writable);
|
||
```
|
||
|
||
Category-based Attribute lookup is available through:
|
||
|
||
```cpp
|
||
static_assert(Property::has_attribute<Unit_Category>);
|
||
const auto& attribute = temperature.attribute<Unit_Category>();
|
||
```
|
||
|
||
All declared Attributes can be traversed:
|
||
|
||
```cpp
|
||
temperature.for_each_attribute([](const auto& attribute) {
|
||
// inspect attribute type/value
|
||
});
|
||
```
|
||
|
||
Constraints can be traversed separately:
|
||
|
||
```cpp
|
||
temperature.for_each_constraint([](const auto& constraint_value) {
|
||
// inspect or evaluate a constraint
|
||
});
|
||
```
|
||
|
||
## 7. Object defaults and effective Attributes
|
||
|
||
`defaults(...)` provides object-wide values for inheritable Attribute categories:
|
||
|
||
```cpp
|
||
defaults(
|
||
external_access<External_Access::read_write>,
|
||
persistence_access<Persistence_Access::load_store>,
|
||
sensitive<false>
|
||
)
|
||
```
|
||
|
||
A property may override a default:
|
||
|
||
```cpp
|
||
field<&Device::name>(
|
||
key<"name">,
|
||
external_access<External_Access::read>
|
||
)
|
||
```
|
||
|
||
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.
|
||
|
||
Extensions can use `declared_effective_attribute<Index, Category>(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category.
|
||
|
||
## 8. Core Attributes
|
||
|
||
### 8.1 Key
|
||
|
||
```cpp
|
||
key<"temperature">
|
||
```
|
||
|
||
Required for every property. Non-empty and unique per schema.
|
||
|
||
### 8.2 External access
|
||
|
||
```cpp
|
||
external_access<External_Access::none>
|
||
external_access<External_Access::read>
|
||
external_access<External_Access::write>
|
||
external_access<External_Access::read_write>
|
||
```
|
||
|
||
This Attribute is inheritable through `defaults(...)`.
|
||
|
||
### 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
|
||
|
||
```cpp
|
||
unit<"C">
|
||
```
|
||
|
||
This is descriptive metadata and is not inheritable.
|
||
|
||
### 8.5 Sensitive
|
||
|
||
```cpp
|
||
sensitive<>
|
||
sensitive<false>
|
||
```
|
||
|
||
This is inheritable metadata. Core exposes its effective value but does not automatically redact data.
|
||
|
||
## 9. Constraints and validation
|
||
|
||
Built-in constraints:
|
||
|
||
```cpp
|
||
min_value<0>
|
||
max_value<100>
|
||
finite
|
||
```
|
||
|
||
Custom constraint:
|
||
|
||
```cpp
|
||
constraint<"even">([](int value) {
|
||
return value % 2 == 0;
|
||
})
|
||
```
|
||
|
||
Validation by member pointer:
|
||
|
||
```cpp
|
||
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||
```
|
||
|
||
Validation by compile-time key:
|
||
|
||
```cpp
|
||
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
|
||
```
|
||
|
||
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. Managed internal read/write
|
||
|
||
Typed member-pointer access:
|
||
|
||
```cpp
|
||
auto temperature = device.read<&Device::temperature>();
|
||
device.write<&Device::temperature>(30.0);
|
||
```
|
||
|
||
Compile-time key access:
|
||
|
||
```cpp
|
||
auto temperature = device.read_key<"temperature">();
|
||
device.write_key<"temperature">(30.0);
|
||
```
|
||
|
||
These operations use `Managed_Access_Mode::internal`.
|
||
|
||
`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:
|
||
|
||
```cpp
|
||
defaults(external_access<External_Access::read_write>)
|
||
```
|
||
|
||
Use:
|
||
|
||
```cpp
|
||
device.external().write<&Device::temperature>(30.0);
|
||
auto value = device.external().read<&Device::temperature>();
|
||
```
|
||
|
||
A const object produces a const capability view and therefore has no write API.
|
||
|
||
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>();
|
||
```
|
||
|
||
Compile-time key forms are also available:
|
||
|
||
```cpp
|
||
device.persistence().load_key<"temperature">(30.0);
|
||
auto value = device.persistence().store_key<"temperature">();
|
||
```
|
||
|
||
The persistence view is controlled by `Persistence_Access` metadata.
|
||
|
||
## 13. Synchronization plans
|
||
|
||
### 13.1 Default independent
|
||
|
||
The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain.
|
||
|
||
Explicit form:
|
||
|
||
```cpp
|
||
synchronization(sync_all_independent)
|
||
```
|
||
|
||
### 13.2 Shared
|
||
|
||
```cpp
|
||
synchronization(sync_all_shared)
|
||
```
|
||
|
||
All properties use the same lock slot unless overridden.
|
||
|
||
### 13.3 Unsynchronized
|
||
|
||
```cpp
|
||
synchronization(sync_all_unsynchronized)
|
||
```
|
||
|
||
Properties use the `unsynchronized_slot` and no real Structive mutex protects them.
|
||
|
||
### 13.4 Per-property override
|
||
|
||
By member pointer:
|
||
|
||
```cpp
|
||
sync_independent<&Device::temperature>()
|
||
sync_unsynchronized<&Device::immutable_id>()
|
||
```
|
||
|
||
Runtime string forms also exist:
|
||
|
||
```cpp
|
||
sync_independent("temperature")
|
||
sync_unsynchronized("immutable_id")
|
||
```
|
||
|
||
Prefer member-pointer rules when the property is statically known.
|
||
|
||
### 13.5 Groups
|
||
|
||
Member-pointer form:
|
||
|
||
```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")
|
||
)
|
||
);
|
||
```
|
||
|
||
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.
|
||
|
||
## 15. Inspect resolved synchronization
|
||
|
||
For a typed member:
|
||
|
||
```cpp
|
||
auto slot = device.lock_slot<&Device::temperature>();
|
||
```
|
||
|
||
For the complete view:
|
||
|
||
```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` identifies unsynchronized properties.
|
||
|
||
## 16. Multi-property static guards
|
||
|
||
Shared 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:
|
||
|
||
```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);
|
||
```
|
||
|
||
The guard only permits access to properties inside its held synchronization set.
|
||
|
||
Capability views provide equivalent static guards constrained by their access mode:
|
||
|
||
```cpp
|
||
auto guard = device.external().lock_shared<&Device::temperature>();
|
||
```
|
||
|
||
## 17. Dynamic-key guards
|
||
|
||
When a property set is known only at runtime:
|
||
|
||
```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
|
||
});
|
||
```
|
||
|
||
Managed value traversal:
|
||
|
||
```cpp
|
||
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
|
||
// one managed read per property
|
||
});
|
||
```
|
||
|
||
Locked traversal acquires the complete readable synchronization set first:
|
||
|
||
```cpp
|
||
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
|
||
// all selected readable properties are held under the guard
|
||
});
|
||
```
|
||
|
||
For coordinated writable access:
|
||
|
||
```cpp
|
||
device.with_all_writable_locked([](auto& guard) {
|
||
// use guard.get / guard.set
|
||
});
|
||
```
|
||
|
||
The persistence view provides `with_all_loadable_locked(...)`.
|
||
|
||
## 19. Computed property
|
||
|
||
A synchronized computed property receives a read view:
|
||
|
||
```cpp
|
||
computed_property<Device, double>([](const auto& view) {
|
||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||
}, key<"speed_span">, external_access<External_Access::read>)
|
||
```
|
||
|
||
The computed property itself and every dependency read through the view must resolve to the same lock slot.
|
||
|
||
For example:
|
||
|
||
```cpp
|
||
synchronization(
|
||
sync_all_independent,
|
||
sync_group("speed", "min_speed", "max_speed", "speed_span")
|
||
)
|
||
```
|
||
|
||
Computed properties are read-only.
|
||
|
||
## 20. Trusted accessor properties
|
||
|
||
Core also supports member-function-based trusted access:
|
||
|
||
```cpp
|
||
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
|
||
```
|
||
|
||
and getter/setter pairs:
|
||
|
||
```cpp
|
||
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
|
||
```
|
||
|
||
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.
|
||
|
||
## 21. Runtime type-erased access
|
||
|
||
Any `Property_Object<T>` is also a `Property_Object_Base`:
|
||
|
||
```cpp
|
||
Property_Object_Base& erased = device;
|
||
```
|
||
|
||
Introspection:
|
||
|
||
```cpp
|
||
erased.runtime_object_type();
|
||
erased.runtime_property_count();
|
||
```
|
||
|
||
Runtime write:
|
||
|
||
```cpp
|
||
double value = 35.0;
|
||
auto result = erased.runtime_write(
|
||
Managed_Access_Mode::external,
|
||
"temperature",
|
||
typeid(double),
|
||
&value
|
||
);
|
||
```
|
||
|
||
Runtime read uses 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(
|
||
Managed_Access_Mode::external,
|
||
"temperature",
|
||
&output,
|
||
&read_double
|
||
);
|
||
```
|
||
|
||
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
|
||
```
|
||
|
||
The runtime path uses the same capability and synchronization rules as typed managed access.
|
||
|
||
## 22. Lock policy
|
||
|
||
Default:
|
||
|
||
```cpp
|
||
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
|
||
};
|
||
```
|
||
|
||
Equivalent shorthand:
|
||
|
||
```cpp
|
||
struct Device : Property_Object<Device> {
|
||
};
|
||
```
|
||
|
||
A no-op lock policy exists:
|
||
|
||
```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:
|
||
|
||
```cpp
|
||
Device& raw = device.unsafe_object();
|
||
```
|
||
|
||
Direct field access is also normal C++:
|
||
|
||
```cpp
|
||
device.temperature = 40.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.
|
||
|
||
## 24. Recommended usage rules
|
||
|
||
- 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.
|