597 lines
13 KiB
Markdown
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.
|