13 KiB
Structive
Structive 在不替代 C++ 原生数据模型的前提下,为普通 struct 增加显式结构元数据和受管理属性行为。
English · 设计理念 · Core 指南 · Extension 指南
Structive 是什么
Structive 是一个 C++20 结构属性系统,明确分成两层:
C++ 对象模型
普通成员、成员函数和原生布局
│
├── Type_Descriptor<T> → Object_Schema
│ 类型级结构、key、属性固有能力、Attribute、Constraint、同步描述
│
└── Property_Object<T>
实例级 managed read/write、同步、遍历和 runtime access
类型本身仍然是普通 C++:
#include <structive/property/property.hpp>
using namespace structive;
struct Device : Property_Object<Device> {
double temperature{25.0};
int serial_number{1001};
};
template <>
struct structive::Type_Descriptor<Device> {
static auto get() {
return object<Device>(
field<&Device::temperature>(key<"temperature">, unit<"C">),
field<&Device::serial_number>(key<"serial_number">, read_only)
);
}
};
成员仍是真实成员。Structive 只在它们旁边增加一层显式结构语义。
核心思想
Structive 的第一原则:
增强 struct,而不是替代 struct。
因此:
- 注册字段仍然是普通 C++ 成员;
- 未注册成员完全不进入 Structive;
- C++ 业务代码优先使用 member pointer 作为编译期身份;
- string key 用于动态系统和 adapter 边界;
- 不要求把字段替换成
Property<T>包装器; - 保留 raw C++ access;
- Structive 不试图给 public member 建立安全边界;
- 外部系统是否暴露、允许读还是允许写,由外部系统自己决定;
- Core 只描述属性本身固有能做什么。
Core 完全不做访问控制
Structive 不再内建 internal、external、persistence、role、context 或 policy 访问模式。
Core 不暴露领域专用访问 View、权限枚举或持久化专用访问模式。
GUI、RPC、序列化器、插件系统、持久化系统都自己决定:
我要不要暴露这个属性?
我要不要允许用户修改?
我要不要保存它?
Structive Core 只回答结构事实:
这个属性自身能不能读?
这个属性自身能不能写?
它的 key 是什么?
它有哪些 Attribute 和 Constraint?
managed access 是否需要同步?
属性自身的 Intrinsic Capability
每个 Property 只有一套固有能力:
none
read
write
read_write
正常情况下由 Accessor 自动推导。普通可写成员天然是 read_write,getter-only computed property 天然是 read。
Member_Accessor 是数据成员的默认实现。对象通过业务方法或子控件暴露属性时,可用 trusted_callable_accessor<Object>(reader, writer) 传入具体访问策略;调用方仍消费统一的 Property_Accessor 协议,不需要复制对象状态或增加转发成员。
Schema 可以显式收窄能力:
field<&Device::serial_number>(
key<"serial_number">,
read_only
)
预定义能力 Attribute:
read_only
write_only
read_write
inaccessible
这些是属性自身契约,不是用户权限。
只读属性可以:
auto id = device.read<&Device::serial_number>();
但:
device.write<&Device::serial_number>(1002);
在编译期就不可用。
如果 C++ 成员本身是 public,raw path 仍然可以:
device.serial_number = 1002;
这代表调用方主动绕过 Structive,同时也绕过 Structive 的同步保证。Structive 采用“君子不防小人”的协作模型,不把自己伪装成 C++ 内存保护机制。
Read-Only 必须带来真正的优化
Property metadata 不只是文档,而应该影响实现。
一个 intrinsic read-only 的存储属性不会进入同步拓扑:
read-only stored property
↓
不分配 lock slot
↓
不贡献 mutex
↓
managed read 不查询 slot
↓
不构造 shared_lock
↓
直接执行 accessor.read()
例如:
struct Device : Property_Object<Device> {
int id{1};
int value{0};
};
template <>
struct structive::Type_Descriptor<Device> {
static auto get() {
return object<Device>(
synchronization(sync_all_shared),
field<&Device::id>(key<"id">, read_only),
field<&Device::value>(key<"value">)
);
}
};
即使默认是 sync_all_shared,id 仍然固定解析成 unsynchronized_slot。只有 value 会为对象贡献 mutex。
前提是调用方遵守 managed contract。如果另一个线程直接写 device.id,那么它已经绕过 Structive,相关 data race 由调用方负责。
Computed Read-Only Property
Computed Property 通常自身不可写,但它可能依赖可写字段:
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">)
Computed value 本身没有可写存储。它的 synchronized view 保护的是可写依赖的一致性域。
- dependency 是 Schema 的显式结构事实,computed view 只能读取已声明的直接依赖;
- read-only 存储依赖可以直接读取,不需要锁;
- writable 依赖如果需要同一快照,只需要彼此处于同一个 synchronization group,computed property 的读取 slot 会从 dependency 自动推导;
- computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定;
- dependency graph 必须是 DAG,Schema 形成时会在编译期拒绝 cycle;
depends_on<>是合法的显式零依赖声明,此类 computed read 固定为 unsynchronized。
Managed Access 与 Raw Access
下面两句语义不同:
device.temperature = 30.0;
device.write<&Device::temperature>(30.0);
第一句是 raw C++ path,第二句是 Structive managed path。
Managed path 使用 Schema 描述的 intrinsic capability 和 synchronization。Raw path 完全绕过这些行为。
Schema 与 Managed Object 分层
Type_Descriptor<T> 描述类型,Property_Object<T> 给实例增加 managed behavior。
默认同步拓扑每个类型只解析并共享一次。实例不再保存默认的 per-property vector。实例只保存真正需要的 mutex storage;只有显式传入 Property_Synchronization 时才保存紧凑的实例级覆盖布局。
No_Lock_Policy 完全不保存真实 mutex。
统一 Property Metadata 模型
Property Descriptor 只有一份 metadata storage,其中可以同时保存 Attribute 和 Constraint。Core 与 Extension 的描述性 metadata 共用 Attribute 协议,Constraint 保持自己的 validation 协议:
struct Label_Category {};
template <Fixed_String Value>
struct Label_Attribute {
using attribute_category = Label_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = false;
static constexpr auto value = Value;
};
Extension metadata 可以直接挂在 Property 上。for_each_metadata() 遍历全部 metadata,for_each_attribute() 只遍历 Attribute,for_each_constraint() 只遍历 Constraint:
field<&Device::temperature>(
key<"temperature">,
presentation::label<"Temperature">
)
Core 负责保存和遍历,但不解释 Extension 自己拥有的 category。
Validation 必须显式
Constraint 是元数据,write() 不自动执行 validation:
auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0);
Validation、transaction、rollback、synchronization 是不同问题,不隐藏在一个 setter 里。
Synchronization
Synchronization 是 topology 层,不是访问控制。它只描述 intrinsically mutable Property 在 managed 并发访问时如何组成一致性域。Stored read-only Property 会在 lock slot 创建前被裁掉,因此属性元数据会直接转化为更低的运行时同步成本。
Structive 提供三种默认规则:
sync_all_independent
sync_all_shared
sync_all_unsynchronized
以及 typed / runtime-key override 和 Group:
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
这些入口并不是重复设计:compile-time member rule 服务 typed C++,runtime-key Plan 服务动态 Adapter,per-instance override 服务少数确实需要特殊 topology 的对象,Guard 服务一次临时的多 Property 一致性操作。它们最终都解析为同一套紧凑 lock-slot 模型。
多 Property Guard 会对 lock domain 去重,并按稳定 slot 顺序获取锁:
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
auto old_min = guard.get<&Device::min_speed>();
auto old_max = guard.get<"maximum_speed">();
guard.set<"minimum_speed">(20);
guard.set<&Device::max_speed>(120);
Typed Guard 的 capability 在编译期通过 constraint 控制;runtime-key Guard 在运行期验证 key 和 capability。详细契约见 Core Guide: Synchronization,对应边界、阻塞和锁顺序测试见 core/tests/synchronization_test.cpp。
Runtime Access
Property_Object_Base 故意保持为底层 type-erased Adapter 边界,面向只有运行期才知道 key 的代码:
Property_Object_Base& erased = device;
auto type = erased.runtime_object_type();
auto count = erased.runtime_property_count();
动态操作只有:
runtime_read(key, context, callback)
runtime_write(key, type_info, value)
返回 ok、unknown_property、not_readable、not_writable、unsupported_runtime_write 或 type_mismatch。Runtime write 是精确类型的 copy-input 边界,不做隐式转换。如果 Accessor 只能接收 move-only 输入,Property 仍然可以保持 intrinsic writable,但会暴露 runtime_copy_writable == false;typed write 仍然支持这种 Property。Read callback 收到的是借用指针,只在 callback 期间有效;对于需要同步的 writable state,callback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。
Core 不在这个边界强制引入 variant、any、转换注册表或 serialization 所有权策略,上层 Adapter 可以按领域需要封装。这里没有 runtime access mode,也没有访问控制;外部系统自行决定暴露策略,Structive 只报告 Property intrinsic capability。详细契约见 Core Guide: Runtime Access,测试见 core/tests/runtime_api_test.cpp。
Core 当前元数据
Core 当前定义:
key<"...">read_onlywrite_onlyread_writeinaccessibleunit<"...">sensitive<>min_value<...>max_value<...>finiteconstraint<"code">(...)
其中四种 capability 只描述 Property 自身,不承担访问控制职责。
当前 Extension 元数据
Presentation Extension 定义:
presentation::label<"...">presentation::description<"...">presentation::group<"...">presentation::order<N>
Presentation consumer 是否显示、是否允许编辑,由 consumer 自己决定,不属于 Property Core。
构建
add_subdirectory(path/to/Structive)
target_link_libraries(my_target PRIVATE structive::property_core)
使用 linked extension:
target_link_libraries(my_target PRIVATE structive::property_extensions)
构建与测试:
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure
独立构建提供 STRUCTIVE_BUILD_EXAMPLES、STRUCTIVE_BUILD_TESTS、STRUCTIVE_INSTALL。Example 只在 Structive 作为顶层工程时默认开启;测试跟随 BUILD_TESTING;独立安装默认开启。安装后可通过 find_package(Structive CONFIG) 使用 structive::property_core 与 structive::property_extensions,并且 standalone CTest 会真实验证外部 install consumer。
测试按契约分工,避免在一个大用例中重复覆盖:
property_core_test.cpp:Schema、Attribute、显式 Validation、typed access、traversal、computed property 与对象复制语义;runtime_api_test.cpp:type-erased result code、callback metadata、copy-write 边界、managed blocking 与只读零锁;synchronization_test.cpp:topology、非法 Plan、Guard held-set、阻塞关系与稳定锁顺序;compile_fail/:重复 key/storage/单值 Attribute、缺失 key、capability/constraint/member 不匹配;- 独立公共头测试与 install consumer:保护 include 自足性和导出包边界。