Files
Structive/docs/CORE_GUIDE.zh-CN.md
2026-08-12 06:16:00 +08:00

20 KiB
Raw Permalink Blame History

Structive Property Core 完整指南

English

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 的结构定义。

3.1 类型级 Metadata

扩展可以直接给被描述类型附加编译期 Attribute;它与 Property metadata、可继承的 Property defaults 相互独立:

struct Action_Category {};
struct Action {
    using attribute_category = Action_Category;
    static constexpr bool single_valued = false;
    static constexpr bool inheritable = false;
    std::string_view name;
};

return object<Device>(
    type_metadata(Action{"reset"}, Action{"calibrate"}),
    field<&Device::temperature>(key<"temperature">)
);

通过 Schema::type_attribute_count<Category>Schema::has_type_attribute<Category>、 单值 category 的 schema.type_attribute<Category>(),以及 schema.for_each_type_attribute<Category>(callback) 查询。Core 只存储并校验通用 Attribute 协议,不解释扩展拥有的 category。类型 metadata 不是 Property default 不会复制到各个 Property。

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;
using Dependencies = Property::dependency_spec;

所有自定义 Property_Accessor 都必须显式声明 using dependency_spec = ...。无依赖时使用 No_Property_Dependencies,不再存在隐式 fallback。

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/writegetter-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 topologyassignment 保留目标对象已经选择的 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() 更适合诊断和框架检查;普通业务代码通常应该通过 readwritelock_sharedlock_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 constraintread-only Property 不能创建 typed unique guardrequires 表达式中也不会看到它的 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 只获取真实存在的同步 slotread-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>(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 声明。depends_on<> 是合法的显式零依赖声明。Computed view 只能读取已声明的直接 dependency。

如果 writable dependency 需要同一快照,只需要让这些 writable dependency 共享 synchronization domain

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 indexschema_dependency_graph_acyclic<Schema>() 暴露 DAG invariantobject_schema(...) 会在编译期拒绝 dependency cycle。

Descriptor 使用一份 metadata tuple 同时保存 Attribute 与 Constraint。for_each_metadata() 遍历两者,for_each_attribute() 只遍历 Attributefor_each_constraint() 只遍历 Constraint。

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 PropertyStructive 在 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 == falseruntime 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_infovoid* 和 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. 推荐规则

  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。