Files
Structive/docs/CORE_GUIDE.md
T
2026-08-07 17:50:04 +08:00

597 lines
13 KiB
Markdown

# Structive Property Core Guide
[中文](CORE_GUIDE.zh-CN.md)
## 1. Include and CMake target
```cpp
#include <structive/property/property.hpp>
```
```cmake
target_link_libraries(my_target PRIVATE structive::property_core)
```
Property Core is C++20.
## 2. Define a managed object
```cpp
using namespace structive;
struct Device : Property_Object<Device> {
double temperature{25.0};
double pressure{101.3};
int serial_number{1001};
};
```
`Property_Object<Device>` adds managed operations. The fields remain ordinary members.
## 3. Define the type descriptor
```cpp
template <>
struct structive::Type_Descriptor<Device> {
static auto get() {
return object<Device>(
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::serial_number>(
key<"serial_number">,
read_only
)
);
}
};
```
The descriptor is the structural definition of `Device`.
## 4. Schema guarantees
A valid schema guarantees:
- 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.
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
```cpp
const auto& schema = type_descriptor<Device>();
const auto& same_schema = device.schema();
```
Properties can be selected by index or member pointer:
```cpp
const auto& first = schema.property<0>();
const auto& temperature = schema.property<&Device::temperature>();
```
## 6. Property descriptor information
A property descriptor exposes compile-time structural facts:
```cpp
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;
```
A `read_only` field reports:
```cpp
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
static_assert(Serial::readable);
static_assert(!Serial::writable);
```
The key is available at runtime without dynamic allocation:
```cpp
auto key_value = schema.property<&Device::temperature>().key();
```
## 7. Intrinsic capability
Core capability values are:
```cpp
Property_Capability::none
Property_Capability::read
Property_Capability::write
Property_Capability::read_write
```
Convenience Attributes are:
```cpp
read_only
write_only
read_write
inaccessible
```
The capability belongs to the property itself. It is not an authorization rule.
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
```
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.
Core only exposes intrinsic `readable` and `writable` facts.
## 9. Core Attributes
### 9.1 Key
Every property requires one key:
```cpp
key<"temperature">
```
The key is the structural protocol identifier used by runtime lookup and adapters.
### 9.2 Capability
```cpp
read_only
write_only
read_write
inaccessible
```
Capability Attributes are single-valued and non-inheritable.
### 9.3 Unit
```cpp
unit<"C">
unit<"kPa">
```
Core stores unit metadata but does not perform conversion.
### 9.4 Sensitive
```cpp
sensitive<>
sensitive<false>
```
`sensitive` is inheritable metadata. It does not implement authorization. A consumer may use it as one input to its own policy.
## 10. Custom Attributes and defaults
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>
max_value<100>
finite
```
Custom constraints:
```cpp
constraint<"even">([](int value) {
return value % 2 == 0;
})
```
Validation is explicit:
```cpp
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
if (result) {
auto key_value = result->property_key;
auto code = result->code;
}
```
`write()` does not automatically call validation.
## 12. Typed managed read/write
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 is also available:
```cpp
auto temperature = device.read_key<"temperature">();
device.write_key<"temperature">(30.0);
```
The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution.
```cpp
template <class Object>
concept Can_Write_Serial = requires(Object& object) {
object.template write<&Device::serial_number>(1);
};
static_assert(!Can_Write_Serial<Device>);
```
## 13. Read-only fast path
Stored read-only properties are statically removed from managed locking.
Given:
```cpp
field<&Device::serial_number>(key<"serial_number">, read_only)
```
Structive resolves:
```text
serial_number -> unsynchronized_slot
```
and `read<&Device::serial_number>()` does not query a lock slot or construct a `shared_lock`.
This remains true even when the broad default is `sync_all_shared`.
If an object has only stored read-only properties, those properties contribute zero mutexes to `resolved_synchronization().lock_count`.
## 14. Synchronization plans
### 14.1 Independent
```cpp
synchronization(sync_all_independent)
```
Each property that actually requires synchronization gets its own lock domain.
### 14.2 Shared
```cpp
synchronization(sync_all_shared)
```
All properties that require synchronization share one lock domain.
Read-only stored properties are excluded before lock slots are materialized.
### 14.3 Unsynchronized
```cpp
synchronization(sync_all_unsynchronized)
```
Managed access performs no real locking even for writable properties.
### 14.4 Per-property override
```cpp
synchronization(
sync_all_independent,
sync_unsynchronized<&Device::temperature>()
)
```
Dynamic-key rule forms are also available.
### 14.5 Groups
```cpp
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
```
Members in one group resolve to the same lock slot if they require synchronization.
## 15. Per-instance synchronization override
An object can explicitly override the type default:
```cpp
Device device{
property_synchronization(
synchronization(sync_all_shared)
)
};
```
For typed member rules:
```cpp
Device device{
property_synchronization<Device>(
synchronization(
sync_all_independent,
sync_group<&Device::temperature, &Device::pressure>("environment")
)
)
};
```
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>();
```
No-lock properties use:
```cpp
Resolved_Synchronization_View::unsynchronized_slot
```
For a read-only stored property this is automatic.
## 17. Static multi-property guards
Read guard:
```cpp
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto temperature = guard.get<&Device::temperature>();
```
Write guard:
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
guard.set<&Device::min_speed>(20);
guard.set<&Device::max_speed>(120);
```
Static guards deduplicate slots and acquire them in stable slot order.
A typed unique guard requires all selected properties to be intrinsically writable.
## 18. Dynamic-key guards
```cpp
auto read_guard = device.lock_shared({"temperature", "pressure"});
auto write_guard = device.lock_unique({"temperature", "pressure"});
```
Unknown keys throw `std::invalid_argument`.
A dynamic unique guard rejects a read-only property because the key is only known at runtime.
## 19. Traversal
Schema-only traversal:
```cpp
schema.for_each_property([&](auto index, const auto& descriptor) {
});
```
Readable value traversal:
```cpp
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
});
```
Locked readable traversal:
```cpp
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
});
```
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) {
});
```
This acquires all writable managed lock domains; it does not perform validation or rollback automatically.
## 20. Computed properties
A synchronized computed property receives a read view:
```cpp
computed_property<Device, int>([](const auto& view) {
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">)
```
Writable dependencies that must form one snapshot should share the computed synchronization domain:
```cpp
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
```
A read-only stored dependency may be read through the computed view without joining a lock slot, because it has no managed writer.
## 21. Trusted accessor properties
Trusted getter:
```cpp
trusted_computed_property<&Device::value>(key<"value">)
```
Trusted getter/setter:
```cpp
trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
```
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.
## 22. Runtime type-erased access
Use `Property_Object_Base` for dynamic adapters:
```cpp
Property_Object_Base& erased = device;
```
Introspection:
```cpp
erased.runtime_object_type();
erased.runtime_property_count();
```
Read:
```cpp
auto result = erased.runtime_read("temperature", context, callback);
```
Write:
```cpp
auto result = erased.runtime_write("temperature", typeid(double), &value);
```
Possible results:
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
Runtime access has no external/persistence mode. An adapter owns its own policy and chooses whether it calls the intrinsic read/write operation.
Runtime read of a stored read-only property follows the same no-lock fast path as typed read.
## 23. Lock policies
Default:
```cpp
Property_Object<Device, Shared_Mutex_Policy>
```
No-lock:
```cpp
Property_Object<Device, No_Lock_Policy>
```
`No_Lock_Policy` stores no real mutex array and avoids real lock objects on managed hot paths.
## 24. Raw object access
```cpp
Device& raw = device.unsafe_object();
```
or normal public member access:
```cpp
device.temperature = 30.0;
```
Both bypass the Structive managed contract.
This is intentional. Structive is cooperative structural infrastructure, not forced encapsulation.
## 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.