首次提交

This commit is contained in:
2026-08-07 16:21:44 +08:00
parent 396311246c
commit 1e903dfe1e
33 changed files with 5960 additions and 1 deletions
+660
View File
@@ -0,0 +1,660 @@
# 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.
+660
View File
@@ -0,0 +1,660 @@
# Structive Property Core 完整指南
[English](CORE_GUIDE.md)
## 1. Include 与 CMake Target
完整 Core 接口:
```cpp
#include <structive/property/property.hpp>
```
CMake
```cmake
target_link_libraries(my_target PRIVATE structive::property_core)
```
Core 是 header-only,要求 C++20。
## 2. 定义 Managed Object
通常让业务对象继承 `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"};
};
```
这些成员仍然都是普通 C++ 成员。
## 3. 定义 Type Descriptor
特化 `Type_Descriptor<T>`,通过 `object<T>(...)` 返回 `Object_Schema`
```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>(...)``property<Member>(...)` 的别名,产生 member-backed `Property_Descriptor`
## 4. Schema 提供的静态保证
Schema 会检查:
- 每个 property 都必须拥有非空 `key`
- 同一 Schema 内 key 唯一;
- 同一个成员存储不能重复注册;
- single-valued Attribute category 不能在同一声明中重复;
- constraint 必须能作用于 property value type
- `defaults(...)` 只能放 inheritable Attribute
- External/Persistence capability 不能声明 accessor 实际不支持的读写能力。
因此大量结构错误会直接成为编译期错误。
## 5. 获取 Schema
类型级:
```cpp
const auto& schema = type_descriptor<Device>();
```
实例级:
```cpp
Device device;
const auto& schema = device.schema();
```
可以通过编译期 index 或成员指针定位 property
```cpp
const auto& first = schema.property<0>();
const auto& temperature = schema.property<&Device::temperature>();
```
业务 typed code 优先成员指针;index 主要用于泛型遍历。
## 6. Property Descriptor
获取 key
```cpp
auto key_value = temperature.key();
```
静态能力:
```cpp
using Property = std::remove_cvref_t<decltype(temperature)>;
static_assert(Property::readable);
static_assert(Property::writable);
```
按 category 查询 Attribute
```cpp
static_assert(Property::has_attribute<Unit_Category>);
const auto& attribute = temperature.attribute<Unit_Category>();
```
遍历全部声明 Attribute
```cpp
temperature.for_each_attribute([](const auto& attribute) {
// 根据 attribute 类型处理
});
```
单独遍历 constraint
```cpp
temperature.for_each_constraint([](const auto& constraint_value) {
// 检查 constraint
});
```
## 7. Object Defaults 与 Effective Attribute
`defaults(...)` 给 inheritable Attribute 提供对象级默认值:
```cpp
defaults(
external_access<External_Access::read_write>,
persistence_access<Persistence_Access::load_store>,
sensitive<false>
)
```
Property 可以覆盖:
```cpp
field<&Device::name>(
key<"name">,
external_access<External_Access::read>
)
```
Core 提供 `effective_external_access_v``external_readable_v``external_writable_v``persistence_loadable_v``persistence_storable_v``effective_sensitive_v` 等有效属性计算入口。
Extension 自己的 inheritable category 可以通过 `declared_effective_attribute<Index, Category>(schema)` 读取 property 声明或 object default 中的有效值。
## 8. Core Attribute
### 8.1 Key
```cpp
key<"temperature">
```
每个 property 必须存在,不能为空,同一 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>
```
支持在 `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>
```
同样支持继承。
### 8.4 Unit
```cpp
unit<"C">
```
描述元数据,不支持 object default 继承。
### 8.5 Sensitive
```cpp
sensitive<>
sensitive<false>
```
支持继承。Core 会计算 effective value,但不会自动执行脱敏或隐藏输出。
## 9. Constraint 与 Validation
内置 constraint
```cpp
min_value<0>
max_value<100>
finite
```
自定义 constraint
```cpp
constraint<"even">([](int value) {
return value % 2 == 0;
})
```
按成员指针验证:
```cpp
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
```
按编译期 key 验证:
```cpp
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
```
失败结果:
```cpp
struct Validation_Error {
std::string_view property_key;
std::string_view code;
};
```
Validation 是显式操作,不会在 managed write 中自动执行。
## 10. Internal Managed Read/Write
成员指针形式:
```cpp
auto temperature = device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
```
编译期 key 形式:
```cpp
auto temperature = device.read_key<"temperature">();
device.write_key<"temperature">(30.0);
```
这些操作属于 `Managed_Access_Mode::internal`
`read()` 返回值对象而不是底层存储引用。启用同步时,读取发生在配置的 shared lock 持有期间。
## 11. External Capability View
可以通过 object defaults 默认开放 external access
```cpp
defaults(external_access<External_Access::read_write>)
```
使用:
```cpp
device.external().write<&Device::temperature>(30.0);
auto value = device.external().read<&Device::temperature>();
```
Const object 返回 const capability view,因此没有 write API。
Typed API 对静态不可访问操作直接在编译期拒绝。
## 12. Persistence Capability View
Persistence 使用 load/store 语义:
```cpp
device.persistence().load<&Device::temperature>(30.0);
auto value = device.persistence().store<&Device::temperature>();
```
编译期 key 版本:
```cpp
device.persistence().load_key<"temperature">(30.0);
auto value = device.persistence().store_key<"temperature">();
```
能力由 `Persistence_Access` 元数据决定。
## 13. Synchronization Plan
### 13.1 Independent
默认 `Synchronization_Plan` 就是 independent:每个同步 property 拥有独立逻辑锁域。
显式写法:
```cpp
synchronization(sync_all_independent)
```
### 13.2 Shared
```cpp
synchronization(sync_all_shared)
```
除显式 override 外,全部 property 共用一个 lock slot。
### 13.3 Unsynchronized
```cpp
synchronization(sync_all_unsynchronized)
```
property 使用 `unsynchronized_slot`Structive 不提供真实 mutex 保护。
### 13.4 单 Property Override
成员指针形式:
```cpp
sync_independent<&Device::temperature>()
sync_unsynchronized<&Device::immutable_id>()
```
运行时字符串形式:
```cpp
sync_independent("temperature")
sync_unsynchronized("immutable_id")
```
静态已知 property 优先成员指针形式。
### 13.5 Group
成员指针形式:
```cpp
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
```
字符串形式:
```cpp
sync_group("speed_range", "min_speed", "max_speed")
```
同一 group 的 property 共用 lock slot。
Resolver 会拒绝未知 key、重复 group name、空 group,以及同一 property 被重复显式配置。
## 14. 每实例覆盖同步策略
Type Descriptor 提供默认同步计划,但单个实例可以通过 `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))
};
```
如果使用成员指针同步规则,可以显式提供对象类型完成 Schema materialization
```cpp
auto policy = property_synchronization<Device>(
synchronization(
sync_all_independent,
sync_group<&Device::temperature, &Device::pressure>("environment")
)
);
```
Schema 本身不变,只改变该实例的 lock topology。
## 15. 查看解析后的同步拓扑
单字段:
```cpp
auto slot = device.lock_slot<&Device::temperature>();
```
完整 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` 表示无同步 property。
## 16. 静态多属性 Guard
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);
```
Guard 只允许访问自己持有同步集合中的 property。
Capability view 也提供受其 access mode 限制的静态 guard
```cpp
auto guard = device.external().lock_shared<&Device::temperature>();
```
## 17. 动态 Key Guard
运行时才知道属性集合时:
```cpp
std::array<std::string_view, 2> keys{"temperature", "pressure"};
auto guard = device.lock_shared(keys);
```
或者:
```cpp
auto guard = device.lock_unique({"min_speed", "max_speed"});
```
动态 guard 会在运行时检查 key 和 capability。获取锁前会对 lock slot 排序、去重。
Guard 获取以后仍然可以通过编译期成员或 key 读写,但访问未包含在 held set 中的 property 会抛出 `std::logic_error`
## 18. Traversal
Schema 遍历:
```cpp
schema.for_each_property([](auto index, const auto& property) {
// compile-time index + descriptor
});
```
Managed value 遍历:
```cpp
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
// 每个 property 单独 managed read
});
```
Locked traversal 会先获取完整 readable 同步集合:
```cpp
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
// 全部选中 property 在 guard 下访问
});
```
集中 writable 操作:
```cpp
device.with_all_writable_locked([](auto& guard) {
// guard.get / guard.set
});
```
Persistence view 对应提供 `with_all_loadable_locked(...)`
## 19. Computed Property
同步 computed property 接收 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>)
```
Computed property 自身与通过 view 读取的每个 dependency 必须解析到同一个 lock slot。
例如:
```cpp
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
```
Computed property 是只读属性。
## 20. Trusted Accessor Property
Core 还支持基于成员函数的 trusted access
```cpp
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
```
以及 getter/setter
```cpp
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
```
它们直接调用对象成员函数,并标记为 trusted object access,不受 synchronized computed view 的 dependency slot 检查。只有当成员函数本身拥有明确同步/一致性契约时才应该使用。
## 21. Runtime Type-Erased Access
任何 `Property_Object<T>` 同时也是 `Property_Object_Base`
```cpp
Property_Object_Base& erased = device;
```
Runtime 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 通过 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
);
```
结果枚举:
```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
```
Runtime path 与 typed managed access 使用同一套 capability 和 synchronization 规则。
## 22. Lock Policy
默认:
```cpp
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
};
```
等价简写:
```cpp
struct Device : Property_Object<Device> {
};
```
也提供 no-op lock policy
```cpp
struct Device : Property_Object<Device, No_Lock_Policy> {
};
```
`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。
## 23. Raw Object Access
`unsafe_object()` 可以拿到底层 derived object
```cpp
Device& raw = device.unsafe_object();
```
普通成员访问当然也仍然存在:
```cpp
device.temperature = 40.0;
```
这些入口故意绕过 Structive managed access。适合调用方已经持有正确同步、或者明确在 Structive 管理契约之外操作的情况。
## 24. 推荐使用规则
- Typed 业务访问优先 member pointer。
- Runtime integration boundary 使用 string key。
- Key 按协议标识对待。
- 稳定的对象级 capability 放 `defaults(...)`,特殊字段单独 override。
- 需要一致观察的字段放同一个 synchronization group。
- Validation 在业务操作边界显式执行。
- 跨字段 invariant 使用静态 multi-property guard。
- 默认优先 synchronized computed property。
- Runtime access 用于 adapter,不要把普通 typed code 动态化。
- Raw access 只在调用方明确拥有被绕过的管理保证时使用。
+424
View File
@@ -0,0 +1,424 @@
# Structive Design Philosophy and Principles
[中文](DESIGN.zh-CN.md)
## 1. Purpose
Structive exists to give ordinary C++ object types a machine-readable structural layer without forcing those types into a framework-specific storage model.
The library should make it possible to ask questions such as:
- Which members are part of the public structural model?
- What is the stable runtime key of a property?
- Which properties are externally readable or writable?
- Which properties participate in persistence?
- Which metadata belongs to presentation, validation or another extension?
- Which properties share a synchronization domain?
- How can generic runtime code read or write a property safely with respect to declared capabilities?
It should answer those questions while leaving the underlying type recognizably C++.
## 2. Primary principle: enhance, do not replace
Structive is not a replacement object model. It is a structural layer attached to an existing C++ type.
The preferred shape is:
```cpp
struct Device : structive::Property_Object<Device> {
double temperature;
std::string name;
};
```
not:
```cpp
struct Device {
Framework_Property<double> temperature;
Framework_Property<std::string> name;
};
```
This principle protects several properties of normal C++ code:
1. Members remain real members.
2. Member pointers remain meaningful identities.
3. Raw access remains possible where ownership rules permit it.
4. Unregistered members can remain implementation details.
5. Generic Structive metadata can evolve independently from storage representation.
The cost of this principle is intentional: Structive cannot intercept raw member access. Managed behavior only applies when callers use the managed path.
## 3. Two layers, not one
Structive separates type-level description from instance-level management.
### 3.1 Type layer
`Type_Descriptor<T>` produces an `Object_Schema` describing:
- registered properties;
- declared property keys;
- accessors;
- Attributes;
- constraints;
- object-level inheritable defaults;
- a default synchronization plan.
This is the structural definition of the type.
### 3.2 Instance layer
`Property_Object<T>` provides:
- resolved lock-slot topology;
- per-instance mutex storage;
- managed typed reads and writes;
- capability views;
- static and runtime multi-property guards;
- traversal over managed values;
- type-erased runtime access through `Property_Object_Base`.
This is instance behavior, not schema identity.
### 3.3 Design rule
Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy.
A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open.
## 4. Registration is explicit
Structive does not assume every C++ member belongs to the structural model.
```cpp
struct Device : structive::Property_Object<Device> {
int id;
double temperature;
mutable int internal_cache;
};
```
If only `id` and `temperature` are registered, `internal_cache` is invisible to Structive.
This is deliberate. Structural exposure is an API decision and should not be inferred from physical layout.
### Rule
**The schema is the public structural contract; the struct layout is not automatically the schema.**
## 5. Prefer compile-time identity inside C++
Business C++ code should normally identify member-backed properties by member pointer:
```cpp
device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
device.schema().property<&Device::temperature>();
```
This gives the compiler the strongest relationship between the object type and the selected field.
Numeric indexes are useful for generic compile-time traversal. String keys are useful at runtime boundaries.
### Identity hierarchy
```text
business C++ code → member pointer
compile-time generic code → property index
runtime/adapters → declared string key
```
Do not unnecessarily convert compile-time code to strings merely because a key exists.
## 6. Keys are structural protocol identifiers
Every `Property_Descriptor` requires a non-empty `key` Attribute. Keys must be unique inside one schema.
A key is not merely a UI label. It is the runtime identity used by schema lookup, dynamic lock selection and runtime access.
Changing a key may therefore change an external protocol or persistence contract even when the C++ member name stays unchanged.
### Rule
**Treat property keys as protocol-level names. Rename them deliberately.**
## 7. One Attribute system
Structive has one Attribute protocol. It should not grow parallel metadata channels such as “hint”, “annotation”, “UI metadata” and “serializer metadata” with separate storage machinery.
An Attribute may declare:
- `attribute_category` for category-based lookup;
- `single_valued` when only one Attribute of that category may appear in a declaration;
- `inheritable` when it may participate in object defaults;
- any payload required by the category owner.
Core and extensions use this same mechanism.
### Rule
**Add semantics by adding an Attribute category and an interpreter, not by adding a second metadata framework.**
## 8. Category ownership
Core should interpret only categories that belong to Core.
For example, Core understands access and persistence capability because those affect Core-managed views. It does not need to understand `presentation::label`.
Presentation owns presentation semantics. A future JSON extension should own JSON-specific semantics. An RPC extension should own RPC-specific semantics.
The Core still stores and traverses all Attributes uniformly.
### Dependency rule
```text
extension implementation
Structive Property Core
```
Never reverse this dependency merely to make an extension convenient.
## 9. Defaults are inheritance, not hidden mutation
`defaults(...)` supports inheritable Attribute categories. Property declarations remain able to override those defaults.
Current Core inheritable categories include external access, persistence access and sensitivity.
This mechanism should be used for stable object-wide policy defaults, not as a general mechanism for implicit behavior.
### Rule
**Defaults should reduce repetition without making a propertys effective policy impossible to determine from schema rules.**
## 10. Managed access and raw access are distinct contracts
Raw access:
```cpp
device.temperature = 30.0;
```
Managed access:
```cpp
device.write<&Device::temperature>(30.0);
```
The raw path is normal C++. It does not acquire Structive locks or enforce Structive capability views.
The managed path uses the descriptor and synchronization topology.
This duality is intentional rather than accidental.
### Rule
Do not pretend that inheriting `Property_Object<T>` turns public C++ members into encapsulated properties. If a subsystem requires managed synchronization, its coding rules must require the managed path.
## 11. Capabilities are views, not copies of the object model
The same property may have different visibility under different managed modes:
```text
internal
external
persistence
```
Core derives those capabilities from Attributes and the accessors actual read/write abilities.
An external view should not become a second schema. A persistence view should not become a second schema. They are projections over one schema.
### Rule
**One property definition, multiple capability projections.**
## 12. Validation is metadata plus an explicit operation
Structive constraints describe candidate validity, but `write()` does not automatically run them.
This separation is essential because these are different concerns:
```text
single-property constraint
cross-property invariant
locking
transaction boundary
rollback strategy
error reporting
side effects
```
A generic `write()` cannot correctly guess all of them.
### Example
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
auto old_min = guard.get<&Device::min_speed>();
auto old_max = guard.get<&Device::max_speed>();
guard.set<&Device::min_speed>(candidate_min);
guard.set<&Device::max_speed>(candidate_max);
if (candidate_min > candidate_max) {
guard.set<&Device::min_speed>(old_min);
guard.set<&Device::max_speed>(old_max);
}
```
The business operation owns the invariant and rollback semantics.
### Rule
**Do not turn `write()` into a hidden transaction engine.**
## 13. Synchronization is synchronization only
A synchronization plan maps properties to lock slots. It supports independent, shared, unsynchronized and grouped configurations.
The purpose of a group is to define a consistency domain: properties in the group share a mutex slot.
Multi-property lock operations deduplicate slots and acquire them in stable slot order.
### Synchronization does not mean
- validation;
- transaction;
- rollback;
- event emission;
- dirty tracking;
- persistence commit.
Those may be built above Structive, but should not be silently coupled to locking.
## 14. Computed properties must have explicit consistency boundaries
`computed_property` reads through a synchronized view. The view only permits access to properties in the same resolved lock slot.
Therefore a computed property that depends on `min_speed` and `max_speed` should share their synchronization group:
```cpp
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
```
This makes consistency explicit rather than relying on a getter that casually reads unrelated fields.
### Trusted accessors
`trusted_computed_property` and `trusted_accessor_property` invoke trusted member functions on the object. They bypass synchronized-view dependency checking by design.
### Rule
**Use synchronized computed properties by default. Use trusted accessors only when the accessor itself owns or guarantees the required synchronization semantics.**
## 15. Runtime access is a boundary feature
`Property_Object_Base` intentionally provides a type-erased runtime interface using:
- string key;
- `Managed_Access_Mode`;
- `std::type_info`;
- explicit result codes.
This is appropriate for adapters that do not know the concrete type at compile time.
It is not a reason to make normal typed C++ code dynamic.
### Rule
**Keep compile-time code compile-time; cross dynamic boundaries only where the application actually has a dynamic boundary.**
## 16. Runtime errors and compile-time errors have different jobs
Structive uses compile-time rejection where the selection is statically known:
- missing member registration;
- duplicate keys;
- duplicate member storage identity;
- invalid Attribute category duplication;
- non-inheritable Attributes inside `defaults(...)`;
- impossible access capability for an accessor.
Runtime failures are reserved for runtime inputs and runtime configuration:
- unknown dynamic key;
- inaccessible dynamic property;
- invalid synchronization plan key;
- duplicate runtime synchronization configuration;
- runtime type mismatch.
### Rule
**Do not defer a statically knowable schema error to runtime. Do not force dynamic adapter input into compile-time machinery.**
## 17. Synchronization policy should remain replaceable
`Property_Object<Derived, Lock_Policy>` supports a lock policy, with `Shared_Mutex_Policy` as the default and `No_Lock_Policy` available.
This is an important architectural seam. Core semantics should not become inseparable from one specific mutex type or one global scheduler.
`No_Lock_Policy` removes actual mutual exclusion; it does not magically make concurrent access safe.
## 18. Extension design rules
A good Structive extension should:
1. Define categories that belong to its own domain.
2. Reuse the Core Attribute protocol.
3. Interpret only the categories it owns or explicitly depends on.
4. Depend on Core, never require Core to depend on it.
5. Keep domain-specific fallback behavior inside the extension.
6. Prefer reading the existing schema over duplicating a parallel descriptor tree.
7. Avoid changing Core behavior merely because an adapter needs a convenience rule.
## 19. Non-goals
Structive Core should not become all of the following at once:
- an ORM;
- a JSON library;
- a GUI framework;
- an RPC framework;
- a signal/slot system;
- a transaction engine;
- a general-purpose reflection replacement for every language feature.
Structive should provide a strong structural contract that those systems can consume.
## 20. Evolution rules
When changing the library, apply these questions in order:
1. Does this belong to the structural model, instance management, or an extension?
2. Is this new concept really an Attribute category rather than a new metadata channel?
3. Can the error be detected at compile time?
4. Does the change preserve native C++ member semantics?
5. Does it preserve the distinction between raw and managed access?
6. Is synchronization being mixed with validation, transaction or events?
7. Does Core now know something that should belong to an extension?
8. Does the API still make member pointers the natural typed identity?
9. Does a new convenience API duplicate an existing semantic path?
10. Does the change preserve existing function-signature meaning rather than silently redefining it?
If a feature fails these questions, reconsider its layer before implementing it.
## 21. Architectural summary
Structive should remain a small number of strong concepts rather than a large number of magical conveniences:
```text
ordinary C++ type
+ explicit schema
+ one Attribute model
+ explicit capabilities
+ explicit validation
+ explicit synchronization
+ optional managed instance behavior
+ extension-owned interpretation
```
The library is strongest when generic systems can understand a type without taking ownership of that type.
+424
View File
@@ -0,0 +1,424 @@
# Structive 设计理念与原则
[English](DESIGN.md)
## 1. 设计目标
Structive 的目标是给普通 C++ 对象增加一层机器可理解的结构语义,同时不强迫业务对象改用框架自己的存储模型。
它应该能够回答:
- 哪些成员属于公开结构模型?
- 某个属性稳定的 runtime key 是什么?
- 哪些属性允许外部读取或写入?
- 哪些属性参与持久化?
- 哪些元数据属于 Presentation、Validation 或其他扩展?
- 哪些属性位于同一个同步域?
- 动态适配器如何在遵守 capability 的前提下读写属性?
但完成这些事情以后,底层类型仍然应该看起来像正常的 C++。
## 2. 第一原则:增强,而不是替代
Structive 不是第二套对象模型,而是附加在已有 C++ 类型旁边的结构层。
推荐形态:
```cpp
struct Device : structive::Property_Object<Device> {
double temperature;
std::string name;
};
```
而不是:
```cpp
struct Device {
Framework_Property<double> temperature;
Framework_Property<std::string> name;
};
```
这个原则保护了原生 C++ 的几个关键性质:
1. 成员仍然是真实成员。
2. 成员指针仍然可以作为可靠身份。
3. 所有权规则允许时仍然可以 raw access。
4. 未注册成员可以继续作为实现细节存在。
5. Structive 元数据可以和存储形式独立演进。
这个原则也有明确代价:Structive 不可能拦截直接成员访问。只有通过 managed path 的访问才会获得 Structive 管理行为。
## 3. 必须保持两层,而不是揉成一层
Structive 把类型级描述和实例级管理分开。
### 3.1 类型层
`Type_Descriptor<T>` 产生 `Object_Schema`,描述:
- 注册属性;
- 属性 key
- accessor
- Attribute
- constraint
- object 级可继承默认值;
- 默认 synchronization plan。
这是类型的结构定义。
### 3.2 实例层
`Property_Object<T>` 提供:
- 解析后的 lock slot 拓扑;
- 每实例 mutex 存储;
- managed typed read/write
- capability view
- 静态与运行时多属性 guard
- managed value traversal
- `Property_Object_Base` type-erased runtime access。
这是实例行为,不是 Schema 身份。
### 3.3 设计约束
不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。
以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。
## 4. 注册必须显式
Structive 不认为所有 C++ 成员都天然属于结构模型。
```cpp
struct Device : structive::Property_Object<Device> {
int id;
double temperature;
mutable int internal_cache;
};
```
如果 Schema 只注册 `id``temperature`,那么 `internal_cache` 对 Structive 完全不可见。
这是故意的。结构暴露本身就是 API 设计,不应该根据物理布局自动推断。
### 原则
**Schema 才是公开结构契约;struct 的物理成员集合不是自动 Schema。**
## 5. C++ 内部优先使用编译期身份
业务 C++ 代码通常应该通过成员指针定位 member-backed property
```cpp
device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
device.schema().property<&Device::temperature>();
```
这样编译器能够建立最强的“对象类型—字段”关系。
数字 index 适合编译期泛型遍历;字符串 key 适合运行时边界。
### 身份层级
```text
业务 C++ 代码 → member pointer
编译期泛型代码 → property index
runtime / adapter → declared string key
```
不要因为存在 key,就把本来可以静态确定的 C++ 业务代码全部字符串化。
## 6. Key 是结构协议标识,不是显示名称
每个 `Property_Descriptor` 都必须拥有非空 `key`,同一 Schema 内 key 必须唯一。
Key 被 Schema lookup、动态锁选择、runtime read/write 使用。因此它不只是 UI label。
即使 C++ 成员名不变,修改 key 也可能改变外部协议或持久化契约。
### 原则
**把 property key 当成协议级名字,重命名必须有意识。**
## 7. 只有一套 Attribute 系统
Structive 不应该再长出 `hint``annotation``ui metadata``serializer metadata` 等彼此独立的元数据容器。
一个 Attribute 可以声明:
- `attribute_category`:用于按 category 查询;
- `single_valued`:同一个声明中该 category 是否只能有一个值;
- `inheritable`:是否允许进入 object defaults
- category 自己需要的 payload。
Core 和 Extension 使用完全相同的协议。
### 原则
**增加语义时优先增加 Attribute category + interpreter,而不是再造一套 metadata framework。**
## 8. Category 必须有明确所有者
Core 只解释属于 Core 的 category。
例如 External Access 和 Persistence Access 会直接影响 Core capability view,所以 Core 理解它们是合理的;但 Core 不需要理解 `presentation::label`
Presentation 解释 Presentation;未来 JSON 扩展解释 JSONRPC 扩展解释 RPC。
Core 只负责统一保存、遍历和提供查询机制。
### 依赖约束
```text
Extension implementation
Structive Property Core
```
不能为了某个扩展写起来方便,就反过来让 Core 依赖 Extension。
## 9. Defaults 是继承,不是隐藏行为
`defaults(...)` 用于支持可继承 Attribute。Property 级声明仍然可以覆盖同 category 的默认值。
当前 Core 中可继承的 category 包括 External Access、Persistence Access 和 Sensitive。
Defaults 应该用于稳定的对象级政策默认值,而不是用来制造大量隐式行为。
### 原则
**Defaults 用来减少重复,但不能让一个属性最终生效的政策变得无法从 Schema 规则中推导。**
## 10. Managed Access 与 Raw Access 是两种契约
Raw access
```cpp
device.temperature = 30.0;
```
Managed access
```cpp
device.write<&Device::temperature>(30.0);
```
Raw path 就是普通 C++,不会自动获得 Structive 锁和 capability 管理。
Managed path 会经过 descriptor 和该实例的同步拓扑。
两条路径同时存在是设计结果,不是漏洞。
### 原则
不要假装继承 `Property_Object<T>` 以后 public member 就自动变成强封装属性。如果某个子系统要求受管理同步,那么该子系统自己的编码约束必须要求使用 managed path。
## 11. Capability 是 Schema 的投影视图,不是第二套 Schema
同一个属性在不同 managed mode 下可以拥有不同可见性:
```text
internal
external
persistence
```
Core 根据 Attribute 和 accessor 实际读写能力计算这些 capability。
External view 不应该发展成第二份 SchemaPersistence view 也不应该成为第二份 Schema。它们都只是同一 Schema 的能力投影。
### 原则
**一份 property definition,多种 capability projection。**
## 12. Validation = 元数据 + 显式操作
Constraint 描述候选值是否合法,但 `write()` 不会自动执行它。
必须保持分开的概念包括:
```text
单字段 constraint
跨字段 invariant
locking
transaction boundary
rollback strategy
error reporting
side effects
```
一个通用 `write()` 不可能替所有业务正确猜出这些规则。
### 示例
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
auto old_min = guard.get<&Device::min_speed>();
auto old_max = guard.get<&Device::max_speed>();
guard.set<&Device::min_speed>(candidate_min);
guard.set<&Device::max_speed>(candidate_max);
if (candidate_min > candidate_max) {
guard.set<&Device::min_speed>(old_min);
guard.set<&Device::max_speed>(old_max);
}
```
业务操作自己拥有 invariant 和 rollback 语义。
### 原则
**不要把 `write()` 变成隐藏事务引擎。**
## 13. Synchronization 就只负责 Synchronization
Synchronization plan 的本质是把 property 映射到 lock slot,支持 independent、shared、unsynchronized、grouped 等配置。
Group 表示一致性域:同一个 group 的 property 使用同一个 mutex slot。
多属性锁会对 slot 去重,并按稳定 slot 顺序获取锁。
### Synchronization 不等于
- validation
- transaction
- rollback
- event emission
- dirty tracking
- persistence commit。
这些能力可以构建在 Structive 上层,但不能偷偷和锁耦合。
## 14. Computed Property 必须明确一致性边界
`computed_property` 通过 synchronized view 读取依赖项。这个 view 只允许读取和 computed property 本身处于同一个 resolved lock slot 的属性。
所以依赖 `min_speed``max_speed``speed_span` 应该和它们进入同一个同步组:
```cpp
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
```
这样一致性关系由同步计划明确表达,而不是让 getter 随意读取任意字段。
### Trusted Accessor
`trusted_computed_property``trusted_accessor_property` 会直接调用对象上的受信任成员函数,设计上绕开 synchronized view 的依赖检查。
### 原则
**默认优先 synchronized computed property。只有 accessor 自身明确拥有或保证同步语义时才使用 trusted accessor。**
## 15. Runtime Access 是边界能力
`Property_Object_Base` 提供 type-erased runtime interface,核心输入包括:
- string key
- `Managed_Access_Mode`
- `std::type_info`
- 显式 result code。
它适用于编译期不知道具体类型的 adapter。
它不应该成为把所有正常 typed C++ 代码动态化的理由。
### 原则
**能在编译期确定的代码就留在编译期;只有真正跨动态边界时才进入 runtime path。**
## 16. Compile-time Error 和 Runtime Error 分工明确
Structive 对静态可知问题尽量在编译期拒绝:
- member 未注册;
- key 重复;
- 同一成员存储被重复注册;
- single-valued category 重复;
- 非 inheritable Attribute 被放入 `defaults(...)`
- accessor 实际能力与声明 capability 冲突。
Runtime failure 留给 runtime 输入和 runtime 配置:
- 动态 key 不存在;
- 动态属性对该 view 不可访问;
- synchronization plan 引用了未知 key
- runtime 同步规则重复配置同一个 property;
- runtime type mismatch。
### 原则
**静态可知的 Schema 错误不要拖到运行时;真正动态的 adapter 输入也不要硬伪装成编译期问题。**
## 17. 同步策略必须保持可替换
`Property_Object<Derived, Lock_Policy>` 支持同步策略,默认是 `Shared_Mutex_Policy`,同时提供 `No_Lock_Policy`
这是重要架构缝隙。Core 语义不应该和某一种 mutex 或某一个全局 scheduler 焊死。
`No_Lock_Policy` 只是取消真实互斥,不代表并发访问自动安全。
## 18. Extension 设计规则
一个好的 Structive Extension 应该:
1. 定义属于自己领域的 category。
2. 复用 Core Attribute 协议。
3. 只解释自己拥有或明确依赖的 category。
4. 依赖 Core,而不是要求 Core 反向依赖自己。
5. 把领域 fallback 行为留在扩展里。
6. 优先消费已有 Schema,不复制第二棵 descriptor tree。
7. 不因为 adapter 需要便利规则就修改 Core 基础语义。
## 19. Core 的非目标
Structive Core 不应该同时变成:
- ORM
- JSON 库;
- GUI 框架;
- RPC 框架;
- signal/slot 系统;
- 事务引擎;
- 替代所有语言能力的“万能反射”。
Structive 应该提供足够强的结构契约,让这些系统来消费它。
## 20. 演进原则
修改库时依次问:
1. 这是 structural model、instance management 还是 extension 的职责?
2. 这个新概念本质上是不是一个 Attribute category,而不是新的 metadata channel
3. 这个错误能否在编译期发现?
4. 修改后是否仍然保留原生 C++ 成员语义?
5. Raw access 与 managed access 的边界是否仍然清晰?
6. 是否把 synchronization 和 validation、transaction、event 混在一起了?
7. Core 是否开始理解本应属于 Extension 的领域概念?
8. Member pointer 是否仍然是 typed API 最自然的身份?
9. 新的 convenience API 是否重复制造了另一条同语义路径?
10. 是否保持了已有函数签名和功能语义,而不是悄悄改义?
如果一个功能连续违反这些问题,应先重新考虑它属于哪一层,而不是立刻实现。
## 21. 架构总结
Structive 最强的形态应该是少量强概念,而不是大量魔法便利接口:
```text
普通 C++ 类型
+ 显式 Schema
+ 一套 Attribute 模型
+ 显式 Capability
+ 显式 Validation
+ 显式 Synchronization
+ 可选 Managed Instance 行为
+ Extension 自己解释自己的语义
```
当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。
+267
View File
@@ -0,0 +1,267 @@
# Structive Extension Architecture
[中文](EXTENSIONS.zh-CN.md)
## 1. Extension role
Structive extensions add domain-specific interpretation without changing the Core structural model.
The intended dependency is:
```text
application / adapter
Structive extension
Structive Property Core
```
Core does not include or link extension code.
## 2. Extension principle
An extension should normally add two things:
1. one or more Attribute categories that express domain metadata;
2. interpretation code that consumes those Attributes.
It should not create a parallel property registry when the existing `Object_Schema` already contains the required structure.
## 3. Current presentation extension
The current extension module defines four Attribute categories:
```cpp
presentation::Label_Category
presentation::Description_Category
presentation::Group_Category
presentation::Order_Category
```
Convenience Attribute values:
```cpp
presentation::label<"Temperature">
presentation::description<"Current device temperature">
presentation::group<"Environment">
presentation::order<10>
```
They can be attached directly to a normal Core property:
```cpp
field<&Device::temperature>(
key<"temperature">,
unit<"C">,
presentation::label<"Temperature">,
presentation::description<"Current device temperature">,
presentation::group<"Environment">,
presentation::order<10>
)
```
No wrapper such as `hint(...)` is required.
## 4. Presentation interpretation
Include:
```cpp
#include <structive/property/extensions/presentation.hpp>
```
Link:
```cmake
target_link_libraries(my_target PRIVATE structive::property_extensions)
```
Describe a property by member pointer:
```cpp
auto info = presentation::describe<&Device::temperature>(device.schema());
```
The result contains:
```cpp
struct Presentation_Info {
std::string_view key;
std::string_view label;
std::string_view description;
std::string_view group;
std::size_t order;
bool has_order;
};
```
If no presentation label is declared, the extension uses the property key as the display label. This fallback is presentation policy and intentionally lives outside Core.
## 5. Inheritable extension Attributes
The presentation `group` Attribute is inheritable, so it may be placed in `defaults(...)`:
```cpp
return object<Device>(
defaults(
external_access<External_Access::read>,
presentation::group<"Environment">
),
field<&Device::temperature>(
key<"temperature">,
presentation::label<"Temperature">
),
field<&Device::pressure>(
key<"pressure">,
presentation::label<"Pressure">
)
);
```
The extension resolves an effective declared value through the same Core default mechanism.
`label`, `description` and `order` are not inheritable and therefore cannot be placed in `defaults(...)`.
## 6. Designing a new extension Attribute
Example:
```cpp
namespace my_adapter {
struct Json_Name_Category {};
template <Fixed_String Value>
struct Json_Name_Attribute {
using attribute_category = Json_Name_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = false;
static constexpr auto value = Value;
};
template <Fixed_String Value>
inline constexpr Json_Name_Attribute<Value> json_name{};
}
```
Use it in a schema:
```cpp
field<&Device::temperature>(
key<"temperature">,
my_adapter::json_name<"temp">
)
```
The Core stores it without needing to know what JSON means.
## 7. Reading extension Attributes
For a property descriptor:
```cpp
using Property = std::remove_cvref_t<decltype(property)>;
if constexpr (Property::has_attribute<my_adapter::Json_Name_Category>) {
const auto& value = property.attribute<my_adapter::Json_Name_Category>();
}
```
For inheritable categories, use effective declared lookup:
```cpp
if constexpr (has_declared_effective_attribute_v<Schema, Index, My_Category>) {
const auto& value = declared_effective_attribute<Index, My_Category>(schema);
}
```
This checks the property declaration first and then object defaults.
## 8. Multi-valued metadata
Core uniqueness is category-driven. An Attribute with `single_valued = true` and a non-void `attribute_category` may only appear once in one declaration.
When a domain needs repeated annotations, design that metadata so it does not claim single-valued category uniqueness, then consume it through `for_each_attribute(...)`.
Do not force naturally repeated metadata into one large unrelated object merely to satisfy a single-valued design.
## 9. Recommended extension boundaries
A future extension may reasonably own metadata and interpretation for areas such as:
- serialization naming and omission rules;
- RPC exposure rules;
- UI labels, groups and editor hints;
- database column mapping;
- configuration-file mapping;
- domain documentation generation.
These are examples of extension domains, not currently implemented features.
The important boundary is that Core should not gain a direct dependency on their libraries or domain types.
## 10. Core metadata may still be consumed
An extension is allowed to interpret Core-owned categories when those categories are part of the extension's input contract.
For example, an external adapter may use:
- property `key` as the default protocol name;
- `External_Access` to determine exposure;
- `sensitive` to decide whether its own logs should omit a value.
The extension may choose a policy based on those attributes, but it should not change what Core itself means by them.
## 11. Fallback behavior belongs to the consumer
The presentation extension demonstrates the intended rule:
```text
no label Attribute
presentation extension chooses property key as label
```
Core does not invent that fallback because a different consumer may want a different behavior.
The same principle should apply to future adapters. Defaults that exist only to make one domain pleasant should remain in that domain.
## 12. Avoid extension-to-Core semantic leakage
Bad direction:
```text
JSON adapter needs alias support
Core adds JSON_Alias_Category and JSON naming logic
```
Preferred direction:
```text
JSON extension defines Json_Name_Category
JSON extension interprets it
Core remains domain-neutral
```
## 13. Linked code versus header-only metadata
Attribute definitions can often be header-only. Interpretation may be header-only or linked depending on implementation needs.
The current presentation extension demonstrates a linked extension target. `presentation::describe()` performs compile-time Attribute selection and calls linked `make_presentation_info()` for the concrete result construction/fallback step.
Do not move linked implementation into Core merely because the extension is small.
## 14. Extension review checklist
Before accepting a new extension feature, ask:
1. Does this concept belong to Core or to one consumer domain?
2. Can it be represented through the existing Attribute protocol?
3. Does the extension own the Attribute category it interprets?
4. Is an inheritable Attribute truly an object-wide default policy?
5. Is fallback behavior kept inside the extension?
6. Is the existing schema reused rather than duplicated?
7. Does the dependency still point from extension to Core?
8. Has Core remained free of third-party domain types?
9. Does the extension preserve the existing meaning of Core access, validation and synchronization?
If the answer to the dependency or semantic-boundary questions is no, the layering should be redesigned before code is added.
+267
View File
@@ -0,0 +1,267 @@
# Structive Extension 架构指南
[English](EXTENSIONS.md)
## 1. Extension 的职责
Structive Extension 的作用是在不修改 Core 结构模型的前提下增加领域语义解释。
推荐依赖方向:
```text
application / adapter
Structive extension
Structive Property Core
```
Core 不 include、不 link Extension。
## 2. Extension 的基本模式
一个 Extension 通常只需要增加两类东西:
1. 一个或多个属于自己领域的 Attribute category
2. 解释这些 Attribute 的实现代码。
如果已有 `Object_Schema` 已经包含需要的结构信息,就不应该再复制第二份 property registry。
## 3. 当前 Presentation Extension
当前扩展模块定义四个 Attribute category
```cpp
presentation::Label_Category
presentation::Description_Category
presentation::Group_Category
presentation::Order_Category
```
对应便利 Attribute
```cpp
presentation::label<"Temperature">
presentation::description<"Current device temperature">
presentation::group<"Environment">
presentation::order<10>
```
它们直接挂到普通 Core property 上:
```cpp
field<&Device::temperature>(
key<"temperature">,
unit<"C">,
presentation::label<"Temperature">,
presentation::description<"Current device temperature">,
presentation::group<"Environment">,
presentation::order<10>
)
```
不需要 `hint(...)` 一类额外包装。
## 4. Presentation 解释
Include
```cpp
#include <structive/property/extensions/presentation.hpp>
```
Link
```cmake
target_link_libraries(my_target PRIVATE structive::property_extensions)
```
通过成员指针描述 property
```cpp
auto info = presentation::describe<&Device::temperature>(device.schema());
```
返回:
```cpp
struct Presentation_Info {
std::string_view key;
std::string_view label;
std::string_view description;
std::string_view group;
std::size_t order;
bool has_order;
};
```
没有声明 presentation label 时,Presentation Extension 使用 property key 作为显示 label。这个 fallback 是 Presentation 自己的政策,不属于 Core。
## 5. 可继承 Extension Attribute
`presentation::group` 是 inheritable,因此可以放入 `defaults(...)`
```cpp
return object<Device>(
defaults(
external_access<External_Access::read>,
presentation::group<"Environment">
),
field<&Device::temperature>(
key<"temperature">,
presentation::label<"Temperature">
),
field<&Device::pressure>(
key<"pressure">,
presentation::label<"Pressure">
)
);
```
Extension 通过 Core 同一套 default 机制解析有效声明。
`label``description``order` 不是 inheritable,因此不能放进 `defaults(...)`
## 6. 设计一个新的 Extension Attribute
例如:
```cpp
namespace my_adapter {
struct Json_Name_Category {};
template <Fixed_String Value>
struct Json_Name_Attribute {
using attribute_category = Json_Name_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = false;
static constexpr auto value = Value;
};
template <Fixed_String Value>
inline constexpr Json_Name_Attribute<Value> json_name{};
}
```
在 Schema 中直接使用:
```cpp
field<&Device::temperature>(
key<"temperature">,
my_adapter::json_name<"temp">
)
```
Core 负责存储它,但完全不需要知道 JSON 是什么。
## 7. 读取 Extension Attribute
单个 property descriptor
```cpp
using Property = std::remove_cvref_t<decltype(property)>;
if constexpr (Property::has_attribute<my_adapter::Json_Name_Category>) {
const auto& value = property.attribute<my_adapter::Json_Name_Category>();
}
```
可继承 category 使用 effective declared lookup
```cpp
if constexpr (has_declared_effective_attribute_v<Schema, Index, My_Category>) {
const auto& value = declared_effective_attribute<Index, My_Category>(schema);
}
```
它先检查 property declaration,再检查 object defaults。
## 8. Multi-Valued Metadata
Core 的唯一性由 category 协议决定。拥有非 void `attribute_category``single_valued = true` 的 Attribute,在同一个声明中只能出现一次。
如果某个领域天然需要重复 annotation,就不要错误地宣称它是 single-valued category;可以通过 `for_each_attribute(...)` 遍历并消费多项数据。
不要为了满足 single-valued 设计,把本来可以重复的独立元数据硬塞进一个巨大对象。
## 9. 合理的未来 Extension 边界
未来 Extension 可以合理拥有例如:
- serialization name / omission policy
- RPC exposure rule
- UI label / group / editor hint
- database column mapping
- configuration-file mapping
- domain documentation generation。
这些只是合理领域示例,不代表当前已经实现。
关键边界是 Core 不能因此直接依赖这些领域库或类型。
## 10. Extension 可以消费 Core Metadata
Extension 可以读取 Core-owned category,只要这些 category 本来就是它的输入契约。
例如 External Adapter 可以使用:
- property `key` 作为默认协议名字;
- `External_Access` 判断是否暴露;
- `sensitive` 决定自己的日志是否隐藏值。
Extension 可以基于这些信息建立自己领域的政策,但不能改变 Core 对这些 Attribute 的基础含义。
## 11. Fallback 行为属于 Consumer
Presentation Extension 当前就是示范:
```text
没有 label Attribute
Presentation Extension 使用 property key 作为 label
```
Core 不应该发明这个 fallback,因为另一个 consumer 完全可能需要另一种策略。
未来 Adapter 也应该遵守这个原则。只为了某个领域“好用”的默认逻辑应该留在那个领域内部。
## 12. 防止 Extension 语义泄漏进 Core
错误方向:
```text
JSON Adapter 需要 alias
Core 增加 JSON_Alias_Category 和 JSON 命名逻辑
```
正确方向:
```text
JSON Extension 定义 Json_Name_Category
JSON Extension 自己解释
Core 保持领域中立
```
## 13. Header-Only Metadata 与 Linked Implementation
Attribute 定义通常可以 header-only。具体解释逻辑可以根据实现需要选择 header-only 或 linked。
当前 Presentation Extension 使用 linked target`presentation::describe()` 在模板层完成 Attribute 选择,然后调用链接实现 `make_presentation_info()` 生成最终结果并处理 fallback。
不能因为扩展代码量小,就把它的 linked implementation 搬进 Core。
## 14. Extension 审核清单
新增 Extension 功能前检查:
1. 这个概念属于 Core,还是只属于某个 consumer domain
2. 能否直接使用现有 Attribute 协议表达?
3. Extension 是否拥有自己解释的 category?
4. Inheritable Attribute 是否真的属于对象级默认政策?
5. Fallback 是否保留在 Extension 内?
6. 是否复用已有 Schema,而不是复制结构树?
7. 依赖是否仍然从 Extension 指向 Core
8. Core 是否仍然没有第三方领域类型?
9. Extension 是否保持了 Core access、validation、synchronization 的原有语义?
如果依赖方向或语义边界不成立,应先重新设计分层,再写代码。