Files
Structive/docs/CORE_GUIDE.zh-CN.md
T
2026-08-11 11:54:09 +08:00

637 lines
18 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 完整指南
[English](CORE_GUIDE.md)
## 1. Include 与 CMake Target
```cpp
#include <structive/property/property.hpp>
```
```cmake
target_link_libraries(my_target PRIVATE structive::property_core)
```
Property Core 使用 C++20。
## 2. 定义 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>` 增加 managed operation,字段本身仍是普通成员。
## 3. 定义 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
)
);
}
};
```
Descriptor 就是 `Device` 的结构定义。
## 4. Schema 静态保证
合法 Schema 保证:
- 每个 Property 都有非空 key
- key 唯一;
- member selector 属于对应 object type
- 同一 Property 上 single-valued Attribute category 不重复;
- capability metadata 不会要求底层 Accessor 不支持的操作;
- Constraint 与 Property value type 兼容。
```cpp
static_assert(Property_Described_Object<Device>);
using Schema = type_descriptor_schema_t<Device>;
static_assert(Valid_Property_Schema<Schema>);
```
## 5. 获取 Schema
```cpp
const auto& schema = type_descriptor<Device>();
const auto& same_schema = device.schema();
```
按 index 或 member pointer 获取 Property
```cpp
const auto& first = schema.property<0>();
const auto& temperature = schema.property<&Device::temperature>();
```
## 6. Property Descriptor
Descriptor 暴露编译期结构事实:
```cpp
using Property = std::remove_cvref_t<decltype(schema.property<&Device::temperature>())>;
static_assert(Property::readable);
static_assert(Property::writable);
static_assert(Property::runtime_copy_writable);
using Value = Property::value_type;
using Accessor = Property::accessor_type;
```
`read_only` 字段:
```cpp
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
static_assert(Serial::readable);
static_assert(!Serial::writable);
```
运行时可直接取得 key
```cpp
auto key_value = schema.property<&Device::temperature>().key();
```
## 7. Intrinsic Capability
Core capability
```cpp
Property_Capability::none
Property_Capability::read
Property_Capability::write
Property_Capability::read_write
```
便捷 Attribute
```cpp
read_only
write_only
read_write
inaccessible
```
Capability 属于 Property 自身,不是 authorization rule。
不声明 capability 时,Structive 根据 Accessor 自动推导。
普通非 const member 默认 read/writegetter-only computed property 默认 read-only。
Capability 可以收窄 Accessor,但不能创造 Accessor 本来不存在的操作。
## 8. Core 不存在访问控制 API
Property Core 不提供 domain access mode,也不区分:
```text
internal
external
persistence
```
Consumer 自己决定 policy。例如 GUI 可以只展示部分 Property,即使这些 Property 在结构上都 readable。
Core 只给出 intrinsic `readable` / `writable`
## 9. Core Attribute
### 9.1 Key
每个 Property 必须有 key
```cpp
key<"temperature">
```
Key 是 runtime lookup 与 adapter 使用的结构协议身份。
### 9.2 Capability
```cpp
read_only
write_only
read_write
inaccessible
```
Capability Attribute 是 single-valued、non-inheritable。
### 9.3 Unit
```cpp
unit<"C">
unit<"kPa">
```
Core 保存单位信息,但不负责换算。
### 9.4 Sensitive
```cpp
sensitive<>
sensitive<false>
```
`sensitive` 是可继承 metadata,不实现访问控制。Consumer 可以把它作为自己 policy 的一个输入。
## 10. 自定义 Attribute 与 Defaults
任何遵守 Attribute protocol 的类型都可以挂在 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 Attribute 可以放进 `defaults(...)`
```cpp
object<Device>(
defaults(sensitive<>),
field<&Device::temperature>(key<"temperature">)
)
```
Capability 故意不可继承,因为每个 Property 的固有操作集合必须独立成立。
## 11. Constraint 与 Validation
内置 Constraint
```cpp
min_value<0>
max_value<100>
finite
```
自定义 Constraint
```cpp
constraint<"even">([](int value) {
return value % 2 == 0;
})
```
显式验证:
```cpp
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
if (result) {
auto key_value = result->property_key;
auto code = result->code;
}
```
`write()` 不自动调用 Validation。
## 12. Typed Managed Read/Write
C++ 业务代码优先 member pointer
```cpp
auto temperature = device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
```
也支持编译期 key
```cpp
auto temperature = device.read<"temperature">();
device.write<"temperature">(30.0);
```
Typed API 由 intrinsic capability 约束。对 `read_only` 调用 write 时,函数在 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 property 在编译期和同步解析阶段都会被裁掉。
```cpp
field<&Device::serial_number>(key<"serial_number">, read_only)
```
最终:
```text
serial_number -> unsynchronized_slot
```
`read<&Device::serial_number>()` 不查询 lock slot,也不构造 `shared_lock`
即使默认是 `sync_all_shared` 也一样。
如果一个对象只有 stored read-only property,这些 Property 对 `resolved_synchronization().lock_count` 的贡献为 0。
## 14. Synchronization Plan
Synchronization 是独立的 topology 层,不是访问权限系统。它只回答一个问题:managed mutable state 并发访问时,哪些 Property 属于同一个一致性域。
Stored intrinsic read-only property 会在 lock slot materialization 之前被移除。因此即使默认规则是 `sync_all_shared`,也不会仅仅因为一个 stored `read_only` Property 而创建 mutex。
### 14.1 默认 Topology
| 默认规则 | 对真正需要同步的 Property 的含义 |
| --- | --- |
| `sync_all_independent` | 每个 Property 独立一个 lock domain |
| `sync_all_shared` | 所有 Property 共用一个 lock domain |
| `sync_all_unsynchronized` | 不创建真实 lock domain |
```cpp
synchronization(sync_all_independent)
```
`sync_all_unsynchronized` 是显式放弃同步。Structive 仍提供 managed access,但并发读写造成的线程安全责任由调用方承担。
### 14.2 编译期 Member Rule
Schema 在 C++ 中已知时优先使用 member pointer
```cpp
synchronization(
sync_all_shared,
sync_independent<&Device::temperature>(),
sync_unsynchronized<&Device::debug_counter>()
)
```
Specification 针对具体 Schema materialize 时会检查 Member 是否真的注册。
### 14.3 Runtime Key Rule
Adapter 或动态配置代码可以使用 key 构造 `Synchronization_Plan`
```cpp
Synchronization_Plan plan;
plan.set_default(Synchronization_Default::independent);
plan.unsynchronized("debug_counter");
```
Key 只能在 resolve 时验证。未知 Property、同一 Property 重复配置、空 Group、重复 Group 名都会抛 `std::invalid_argument`
### 14.4 Group 表示一致性域
```cpp
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
```
同一 Group 中真正需要同步的 Property 会解析到同一个 lock slot。Stored read-only Property 即使被宽泛规则或 Group 包含,也仍然是 `unsynchronized_slot`,因为 managed path 下不存在 writer。
Group 应该表达真实业务 invariant 或 snapshot boundary,而不是单纯为了减少 mutex 数量。
### 14.5 为什么 Synchronization API 有多种入口
这些入口对应的是“信息在什么时候已知”,不是重复抽象:
- 类型级默认规则描述一个类型的正常 topology;
- compile-time member rule 让普通 C++ 代码获得 Schema 编译期检查;
- runtime-key rule 服务动态 Adapter
- per-instance override 服务确实需要特殊 topology 的个别对象;
- Guard 表达一次临时的多 Property 一致性操作。
它们最终都落到同一个 resolved lock-slot topology。
## 15. 每实例 Synchronization Override
对象可以显式覆盖类型默认策略:
```cpp
Device device{
property_synchronization(
synchronization(sync_all_shared)
)
};
```
Typed member rule
```cpp
Device device{
property_synchronization<Device>(
synchronization(
sync_all_independent,
sync_group<&Device::temperature, &Device::pressure>("environment")
)
)
};
```
默认 topology 每类型只 resolve 一次并共享。只有显式 override 的实例才保存紧凑 override layout。Copy/move construction 保留源对象的 override topologyassignment 保留目标对象已经选择的 topology,因为赋值修改的是对象状态,不应该偷偷改变这个实例的同步策略。
## 16. 查看 Resolved Synchronization
```cpp
auto view = device.resolved_synchronization();
auto count = view.lock_count;
```
成员 slot
```cpp
auto slot = device.lock_slot<&Device::temperature>();
```
无锁 Property 使用:
```cpp
Resolved_Synchronization_View::unsynchronized_slot
```
Stored read-only Property 自动得到这个值。`resolved_synchronization()``lock_slot()` 更适合诊断和框架检查;普通业务代码通常应该通过 `read``write``lock_shared``lock_unique` 表达意图,而不是依赖数字 slot。
## 17. 静态多属性 Guard
Read Guard
```cpp
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto by_member = guard.get<&Device::temperature>();
auto by_key = guard.get<"pressure">();
```
Write Guard
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
guard.set<&Device::min_speed>(20);
guard.set<"maximum_speed">(120);
```
Member 和 compile-time key 统一使用同名 `get`/`set`。Capability 直接进入 overload constraintread-only Property 不能创建 typed unique guard`requires` 表达式中也不会看到它的 `set`
Static Guard 会先解析 slot、去重,然后按稳定的数字 slot 顺序获取锁。调用方传入 Member 的顺序不会成为 mutex 获取顺序,因此两个调用方即使按相反 Member 顺序请求同一批 domain,也不会因为 API 参数顺序造成 lock-order inversion。
Stored read-only Property 可以出现在 shared guard 的逻辑 held set 中,但不会因此创建 mutex,仍保持 read-only zero-lock fast path。
## 18. Dynamic-Key Guard
```cpp
auto read_guard = device.lock_shared({"temperature", "pressure"});
auto write_guard = device.lock_unique({"temperature", "pressure"});
auto value = read_guard.get<"temperature">();
```
Dynamic Guard 服务“目标 Property 集合只有运行期才知道”的场景,因此 typed guard 的编译期错误在这里变成运行时错误:
- 未知 key 抛 `std::invalid_argument`
- `lock_shared` 遇到不可读 Property 时拒绝;
- `lock_unique` 遇到不可写 Property 时拒绝;
- 后续 `get`/`set` 请求不在 held set 中的 Property 时抛 `std::logic_error`
Dynamic Guard 和 Static Guard 使用完全相同的 resolved topology 和稳定锁顺序。
这些行为由 `core/tests/synchronization_test.cpp` 单独覆盖,包括 independent/shared/unsynchronized 默认规则、Group、override、非法 Plan、runtime-key Guard、阻塞语义以及相反 Member 顺序获取锁。
## 19. Traversal
只遍历 Schema
```cpp
schema.for_each_property([&](auto index, const auto& descriptor) {
});
```
遍历 readable value
```cpp
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
});
```
一次性锁定后遍历:
```cpp
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
});
```
Locked traversal 只获取真实存在的同步 slotread-only stored property 不会引入锁。
获取全部 writable lock domain
```cpp
device.with_all_writable_locked([&](auto& guard) {
});
```
它不会自动执行 validation 或 rollback。
## 20. Computed Property
Synchronized computed property 接收 read view
```cpp
computed_property<Device, int>(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) {
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">)
```
`depends_on<...>` 把 dependency 变成 Schema 的显式结构事实。也可以通过 `depends_on_keys<"a", "b">` 按 key 声明。Computed view 只能读取已声明的直接 dependency。
如果 writable dependency 需要同一快照,只需要让这些 writable dependency 共享 synchronization domain
```cpp
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed")
)
```
Computed Property 自身不要加入 synchronization rule。它的读取 slot 根据 dependency graph 自动推导;如果需要同步的 dependency 不在同一个 domainSchema/实例 synchronization topology 会被拒绝。Read-only stored dependency 不需要 lock slot。
Schema 可通过 `schema_property_dependency_indices<Schema, Index>()` 读取某个 Property 的直接 dependency index。
## 21. Trusted Accessor Property
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">)
```
这类入口直接访问对象,而不是通过 synchronized dependency view。只有调用方明确掌握成员函数同步语义时才应该使用。
## 22. Runtime Type-Erased Access
`Property_Object_Base` 是动态 Adapter 边界,面向 GUI inspector、serialization adapter、脚本绑定、RPC 层等“只有运行期才知道 Property key”的代码。普通 typed C++ 业务代码仍应优先使用 `read` / `write`
```cpp
Property_Object_Base& erased = device;
```
类型信息:
```cpp
erased.runtime_object_type();
erased.runtime_property_count();
```
### 22.1 Runtime Read
```cpp
auto result = erased.runtime_read("temperature", context, callback);
```
成功时 callback 恰好调用一次,并收到 Schema index、key、精确 `type_info` 以及当前 value 的指针。这个 value pointer 是借用指针,只在 callback 执行期间有效;必须同步消费或复制,不能保存到 callback 之后继续使用。
对于需要同步的 writable PropertyStructive 在 callback 执行期间仍然持有对应的 managed read lock,因此 callback 不应该重入同一 lock domain 的冲突写操作。Stored read-only Property 继续走 intrinsic zero-lock fast path,不会因为入口变成 runtime 就额外创建或获取 mutex。
### 22.2 Runtime Write
```cpp
auto result = erased.runtime_write("temperature", typeid(double), &value);
```
Runtime write 明确不做隐式转换。`typeid(double)` 必须与 Property 声明的 value type 完全一致,`value` 指针在调用期间必须指向这个精确类型的有效对象。这个边界从 `const` 输入复制;如果一个 intrinsically writable Accessor 无法接受 copy-input,它会暴露 `runtime_copy_writable == false`runtime access 返回 `unsupported_runtime_write`,而 typed `write` 在 Accessor 支持时仍可正常接收 move-only 值。成功写入继续复用 typed `write` 的 managed synchronization 路径。
### 22.3 Result Contract
| Result | 含义 |
| --- | --- |
| `ok` | lookup 和访问完成 |
| `unknown_property` | Schema 中不存在该 runtime key |
| `not_readable` | Property 存在,但 intrinsic capability 不可读 |
| `not_writable` | Property 存在,但 intrinsic capability 不可写 |
| `unsupported_runtime_write` | Property intrinsically writable,但 Accessor 无法接收 runtime copy-input 边界 |
| `type_mismatch` | runtime write 提供的类型和 Property value type 不一致 |
这里没有 external/persistence mode,也没有访问控制 policy。Adapter 自己决定是否对外暴露、是否调用 runtime read/writeStructive 只报告 Property 自身的 intrinsic capability。
### 22.4 为什么 Runtime API 故意保持底层
动态边界使用 `type_info``void*` 和 callback,是因为调用方在编译期根本不知道值类型。Core 不强制引入统一 `variant`、堆分配 `any`、serialization 格式或者 conversion registry,因为这些方案都会把所有权和转换策略重新塞回 Core。
因此 Runtime API 是 Adapter API,而不是普通业务代码首选 API。上层 Extension 完全可以按自己的领域需求再包一层 value container,而不用改变 Structive Core。
这些契约由 `core/tests/runtime_api_test.cpp` 单独覆盖,包括全部 result code、callback metadata、精确类型写入、managed blocking 语义,以及 read-only zero-lock runtime fast path。
## 23. Lock Policy
默认:
```cpp
Property_Object<Device, Shared_Mutex_Policy>
```
NoLock
```cpp
Property_Object<Device, No_Lock_Policy>
```
`No_Lock_Policy` 不保存真实 mutex array,并从 managed hot path 去除真实 lock object。
## 24. Raw Object Access
```cpp
Device& raw = device.unsafe_object();
```
或直接 public member
```cpp
device.temperature = 30.0;
```
都绕过 Structive managed contract。
这是刻意设计。Structive 是协作式结构基础设施,不是强制封装。
## 25. 推荐规则
1. 存储保持普通 C++ member。
2. 只注册真正属于结构模型的字段。
3. C++ 业务代码优先 member-pointer typed API。
4. 当 managed model 永远不应该写某个存储字段时,用 `read_only`
5. 利用 stored read-only 的 zero-lock 优化。
6. 并发 managed code 中不要通过 raw path 修改 read-only 字段。
7. 跨字段 mutable consistency 使用 synchronization group。
8. Validation 保持显式。
9. Runtime access 只用于真正动态的 Adapter。
10. GUI/RPC/Persistence/Authorization 自己拥有 exposure 与 access policy。