Files
Structive/README.zh-CN.md
T
2026-08-11 12:40:28 +08:00

13 KiB
Raw Blame History

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 不再内建 internalexternalpersistence、role、context 或 policy 访问模式。

Core 不暴露领域专用访问 View、权限枚举或持久化专用访问模式。

GUI、RPC、序列化器、插件系统、持久化系统都自己决定:

我要不要暴露这个属性?
我要不要允许用户修改?
我要不要保存它?

Structive Core 只回答结构事实:

这个属性自身能不能读?
这个属性自身能不能写?
它的 key 是什么?
它有哪些 Attribute 和 Constraint
managed access 是否需要同步?

属性自身的 Intrinsic Capability

每个 Property 只有一套固有能力:

none
read
write
read_write

正常情况下由 Accessor 自动推导。普通可写成员天然是 read_writegetter-only computed property 天然是 read

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++ 成员本身是 publicraw 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_sharedid 仍然固定解析成 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 groupcomputed property 的读取 slot 会从 dependency 自动推导;
  • computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定;
  • dependency graph 必须是 DAGSchema 形成时会在编译期拒绝 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() 遍历全部 metadatafor_each_attribute() 只遍历 Attributefor_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 服务动态 Adapterper-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)

返回 okunknown_propertynot_readablenot_writableunsupported_runtime_writetype_mismatch。Runtime write 是精确类型的 copy-input 边界,不做隐式转换。如果 Accessor 只能接收 move-only 输入,Property 仍然可以保持 intrinsic writable,但会暴露 runtime_copy_writable == falsetyped write 仍然支持这种 Property。Read callback 收到的是借用指针,只在 callback 期间有效;对于需要同步的 writable statecallback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。

Core 不在这个边界强制引入 variantany、转换注册表或 serialization 所有权策略,上层 Adapter 可以按领域需要封装。这里没有 runtime access mode,也没有访问控制;外部系统自行决定暴露策略,Structive 只报告 Property intrinsic capability。详细契约见 Core Guide: Runtime Access,测试见 core/tests/runtime_api_test.cpp

Core 当前元数据

Core 当前定义:

  • key<"...">
  • read_only
  • write_only
  • read_write
  • inaccessible
  • unit<"...">
  • sensitive<>
  • min_value<...>
  • max_value<...>
  • finite
  • constraint<"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_EXAMPLESSTRUCTIVE_BUILD_TESTSSTRUCTIVE_INSTALL。Example 只在 Structive 作为顶层工程时默认开启;测试跟随 BUILD_TESTING;独立安装默认开启。安装后可通过 find_package(Structive CONFIG) 使用 structive::property_corestructive::property_extensions,并且 standalone CTest 会真实验证外部 install consumer。

测试按契约分工,避免在一个大用例中重复覆盖:

  • property_core_test.cppSchema、Attribute、显式 Validation、typed access、traversal、computed property 与对象复制语义;
  • runtime_api_test.cpptype-erased result code、callback metadata、copy-write 边界、managed blocking 与只读零锁;
  • synchronization_test.cpptopology、非法 Plan、Guard held-set、阻塞关系与稳定锁顺序;
  • compile_fail/:重复 key/storage/单值 Attribute、缺失 key、capability/constraint/member 不匹配;
  • 独立公共头测试与 install consumer:保护 include 自足性和导出包边界。

详细文档