18 KiB
Structive Property Core 完整指南
1. Include 与 CMake Target
#include <structive/property/property.hpp>
target_link_libraries(my_target PRIVATE structive::property_core)
Property Core 使用 C++20。
2. 定义 Managed Object
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
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 兼容。
static_assert(Property_Described_Object<Device>);
using Schema = type_descriptor_schema_t<Device>;
static_assert(Valid_Property_Schema<Schema>);
5. 获取 Schema
const auto& schema = type_descriptor<Device>();
const auto& same_schema = device.schema();
按 index 或 member pointer 获取 Property:
const auto& first = schema.property<0>();
const auto& temperature = schema.property<&Device::temperature>();
6. Property Descriptor
Descriptor 暴露编译期结构事实:
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 字段:
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
static_assert(Serial::readable);
static_assert(!Serial::writable);
运行时可直接取得 key:
auto key_value = schema.property<&Device::temperature>().key();
7. Intrinsic Capability
Core capability:
Property_Capability::none
Property_Capability::read
Property_Capability::write
Property_Capability::read_write
便捷 Attribute:
read_only
write_only
read_write
inaccessible
Capability 属于 Property 自身,不是 authorization rule。
不声明 capability 时,Structive 根据 Accessor 自动推导。
普通非 const member 默认 read/write;getter-only computed property 默认 read-only。
Capability 可以收窄 Accessor,但不能创造 Accessor 本来不存在的操作。
8. Core 不存在访问控制 API
Property Core 不提供 domain access mode,也不区分:
internal
external
persistence
Consumer 自己决定 policy。例如 GUI 可以只展示部分 Property,即使这些 Property 在结构上都 readable。
Core 只给出 intrinsic readable / writable。
9. Core Attribute
9.1 Key
每个 Property 必须有 key:
key<"temperature">
Key 是 runtime lookup 与 adapter 使用的结构协议身份。
9.2 Capability
read_only
write_only
read_write
inaccessible
Capability Attribute 是 single-valued、non-inheritable。
9.3 Unit
unit<"C">
unit<"kPa">
Core 保存单位信息,但不负责换算。
9.4 Sensitive
sensitive<>
sensitive<false>
sensitive 是可继承 metadata,不实现访问控制。Consumer 可以把它作为自己 policy 的一个输入。
10. 自定义 Attribute 与 Defaults
任何遵守 Attribute protocol 的类型都可以挂在 Property 上。
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(...):
object<Device>(
defaults(sensitive<>),
field<&Device::temperature>(key<"temperature">)
)
Capability 故意不可继承,因为每个 Property 的固有操作集合必须独立成立。
11. Constraint 与 Validation
内置 Constraint:
min_value<0>
max_value<100>
finite
自定义 Constraint:
constraint<"even">([](int value) {
return value % 2 == 0;
})
显式验证:
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:
auto temperature = device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
也支持编译期 key:
auto temperature = device.read<"temperature">();
device.write<"temperature">(30.0);
Typed API 由 intrinsic capability 约束。对 read_only 调用 write 时,函数在 overload resolution 阶段就不可用。
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 在编译期和同步解析阶段都会被裁掉。
field<&Device::serial_number>(key<"serial_number">, read_only)
最终:
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 |
synchronization(sync_all_independent)
sync_all_unsynchronized 是显式放弃同步。Structive 仍提供 managed access,但并发读写造成的线程安全责任由调用方承担。
14.2 编译期 Member Rule
Schema 在 C++ 中已知时优先使用 member pointer:
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:
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 表示一致性域
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
对象可以显式覆盖类型默认策略:
Device device{
property_synchronization(
synchronization(sync_all_shared)
)
};
Typed member rule:
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 topology;assignment 保留目标对象已经选择的 topology,因为赋值修改的是对象状态,不应该偷偷改变这个实例的同步策略。
16. 查看 Resolved Synchronization
auto view = device.resolved_synchronization();
auto count = view.lock_count;
成员 slot:
auto slot = device.lock_slot<&Device::temperature>();
无锁 Property 使用:
Resolved_Synchronization_View::unsynchronized_slot
Stored read-only Property 自动得到这个值。resolved_synchronization() 和 lock_slot() 更适合诊断和框架检查;普通业务代码通常应该通过 read、write、lock_shared、lock_unique 表达意图,而不是依赖数字 slot。
17. 静态多属性 Guard
Read Guard:
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto by_member = guard.get<&Device::temperature>();
auto by_key = guard.get<"pressure">();
Write Guard:
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 constraint:read-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
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:
schema.for_each_property([&](auto index, const auto& descriptor) {
});
遍历 readable value:
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
});
一次性锁定后遍历:
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
});
Locked traversal 只获取真实存在的同步 slot,read-only stored property 不会引入锁。
获取全部 writable lock domain:
device.with_all_writable_locked([&](auto& guard) {
});
它不会自动执行 validation 或 rollback。
20. Computed Property
Synchronized computed property 接收 read view:
computed_property<Device, int>([](const auto& view) {
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">)
如果 writable dependency 需要同一快照,应共享 computed synchronization domain:
synchronization(
sync_all_independent,
sync_group("speed", "min_speed", "max_speed", "speed_span")
)
Read-only stored dependency 可以直接通过 computed view 读取,不需要加入 lock slot,因为它没有 managed writer。
21. Trusted Accessor Property
Trusted getter:
trusted_computed_property<&Device::value>(key<"value">)
Trusted getter/setter:
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。
Property_Object_Base& erased = device;
类型信息:
erased.runtime_object_type();
erased.runtime_property_count();
22.1 Runtime Read
auto result = erased.runtime_read("temperature", context, callback);
成功时 callback 恰好调用一次,并收到 Schema index、key、精确 type_info 以及当前 value 的指针。这个 value pointer 是借用指针,只在 callback 执行期间有效;必须同步消费或复制,不能保存到 callback 之后继续使用。
对于需要同步的 writable Property,Structive 在 callback 执行期间仍然持有对应的 managed read lock,因此 callback 不应该重入同一 lock domain 的冲突写操作。Stored read-only Property 继续走 intrinsic zero-lock fast path,不会因为入口变成 runtime 就额外创建或获取 mutex。
22.2 Runtime Write
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/write;Structive 只报告 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
默认:
Property_Object<Device, Shared_Mutex_Policy>
NoLock:
Property_Object<Device, No_Lock_Policy>
No_Lock_Policy 不保存真实 mutex array,并从 managed hot path 去除真实 lock object。
24. Raw Object Access
Device& raw = device.unsafe_object();
或直接 public member:
device.temperature = 30.0;
都绕过 Structive managed contract。
这是刻意设计。Structive 是协作式结构基础设施,不是强制封装。
25. 推荐规则
- 存储保持普通 C++ member。
- 只注册真正属于结构模型的字段。
- C++ 业务代码优先 member-pointer typed API。
- 当 managed model 永远不应该写某个存储字段时,用
read_only。 - 利用 stored read-only 的 zero-lock 优化。
- 并发 managed code 中不要通过 raw path 修改 read-only 字段。
- 跨字段 mutable consistency 使用 synchronization group。
- Validation 保持显式。
- Runtime access 只用于真正动态的 Adapter。
- GUI/RPC/Persistence/Authorization 自己拥有 exposure 与 access policy。