Files
Structive/docs/CORE_GUIDE.md
T
2026-08-07 16:21:44 +08:00

661 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 the instance lock topology changes.
## 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.
## 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 Structives 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.