# Structive Property Core Guide [中文](CORE_GUIDE.zh-CN.md) ## 1. Include and target Include the complete Core surface with: ```cpp #include ``` CMake target: ```cmake target_link_libraries(my_target PRIVATE structive::property_core) ``` The Core target is header-only and requires C++20. ## 2. Define a managed object A managed object normally derives from `Property_Object`: ```cpp using namespace structive; struct Device : Property_Object { double temperature{25.0}; double pressure{101.3}; double min_speed{10.0}; double max_speed{100.0}; std::string name{"device-1"}; }; ``` The members remain ordinary C++ members. ## 3. Define the type descriptor Specialize `Type_Descriptor` and return an `Object_Schema` through `object(...)`: ```cpp template <> struct structive::Type_Descriptor { static auto get() { return object( defaults( external_access, persistence_access ), synchronization( sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range") ), field<&Device::temperature>( key<"temperature">, unit<"C">, min_value<-50.0>, max_value<200.0> ), field<&Device::pressure>(key<"pressure">, unit<"kPa">), field<&Device::min_speed>(key<"min_speed">), field<&Device::max_speed>(key<"max_speed">), field<&Device::name>(key<"name">) ); } }; ``` `field(...)` is an alias of `property(...)` and produces a member-backed `Property_Descriptor`. ## 4. Schema guarantees The schema enforces several structural conditions: - every property has a non-empty `key`; - property keys are unique; - the same member storage identity cannot be registered twice; - single-valued Attribute categories cannot appear more than once on the same declaration; - constraints must be compatible with the property value type; - object defaults may contain only inheritable Attributes; - external/persistence capabilities cannot claim operations unsupported by the accessor. Many schema errors are therefore compile-time errors. ## 5. Access the schema For a described type: ```cpp const auto& schema = type_descriptor(); ``` For an instance: ```cpp Device device; const auto& schema = device.schema(); ``` Property lookup supports a numeric compile-time index or a member pointer: ```cpp const auto& first = schema.property<0>(); const auto& temperature = schema.property<&Device::temperature>(); ``` Use member pointers in typed business code. Use indexes mainly in generic traversal. ## 6. Property descriptor information A property descriptor exposes: ```cpp auto key_value = temperature.key(); ``` and compile-time traits such as: ```cpp using Property = std::remove_cvref_t; static_assert(Property::readable); static_assert(Property::writable); ``` Category-based Attribute lookup is available through: ```cpp static_assert(Property::has_attribute); const auto& attribute = temperature.attribute(); ``` All declared Attributes can be traversed: ```cpp temperature.for_each_attribute([](const auto& attribute) { // inspect attribute type/value }); ``` Constraints can be traversed separately: ```cpp temperature.for_each_constraint([](const auto& constraint_value) { // inspect or evaluate a constraint }); ``` ## 7. Object defaults and effective Attributes `defaults(...)` provides object-wide values for inheritable Attribute categories: ```cpp defaults( external_access, persistence_access, sensitive ) ``` A property may override a default: ```cpp field<&Device::name>( key<"name">, external_access ) ``` Core exposes effective access traits such as `effective_external_access_v`, `external_readable_v`, `external_writable_v`, `persistence_loadable_v`, `persistence_storable_v` and `effective_sensitive_v` for generic code. Extensions can use `declared_effective_attribute(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category. ## 8. Core Attributes ### 8.1 Key ```cpp key<"temperature"> ``` Required for every property. Non-empty and unique per schema. ### 8.2 External access ```cpp external_access external_access external_access external_access ``` This Attribute is inheritable through `defaults(...)`. ### 8.3 Persistence access ```cpp persistence_access persistence_access persistence_access persistence_access ``` This Attribute is also inheritable. ### 8.4 Unit ```cpp unit<"C"> ``` This is descriptive metadata and is not inheritable. ### 8.5 Sensitive ```cpp sensitive<> sensitive ``` This is inheritable metadata. Core exposes its effective value but does not automatically redact data. ## 9. Constraints and validation Built-in constraints: ```cpp min_value<0> max_value<100> finite ``` Custom constraint: ```cpp constraint<"even">([](int value) { return value % 2 == 0; }) ``` Validation by member pointer: ```cpp auto error = validate_property_value<&Device::temperature>(device.schema(), candidate); ``` Validation by compile-time key: ```cpp auto error = validate_property_key_value<"temperature">(device.schema(), candidate); ``` A failure returns: ```cpp struct Validation_Error { std::string_view property_key; std::string_view code; }; ``` Validation is explicit and is not automatically executed by managed writes. ## 10. Intrinsic managed read/write Typed member-pointer access: ```cpp auto temperature = device.read<&Device::temperature>(); device.write<&Device::temperature>(30.0); ``` Compile-time key access: ```cpp auto temperature = device.read_key<"temperature">(); device.write_key<"temperature">(30.0); ``` These operations use the property's intrinsic capability directly. There is no separate `internal` capability mode; readability and writability come from the accessor itself. `read()` returns a value object, not a reference to storage. The read is performed while the configured shared lock is held when synchronization is enabled. ## 11. External capability view Object-level defaults may expose properties externally: ```cpp defaults(external_access) ``` Use: ```cpp device.external().write<&Device::temperature>(30.0); auto value = device.external().read<&Device::temperature>(); ``` A const object produces a const capability view and therefore has no write API. The typed API rejects statically inaccessible operations at compile time. ## 12. Persistence capability view Use persistence terminology instead of generic write/read: ```cpp device.persistence().load<&Device::temperature>(30.0); auto value = device.persistence().store<&Device::temperature>(); ``` Compile-time key forms are also available: ```cpp device.persistence().load_key<"temperature">(30.0); auto value = device.persistence().store_key<"temperature">(); ``` The persistence view is controlled by `Persistence_Access` metadata. ## 13. Synchronization plans ### 13.1 Default independent The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain. Explicit form: ```cpp synchronization(sync_all_independent) ``` ### 13.2 Shared ```cpp synchronization(sync_all_shared) ``` All properties use the same lock slot unless overridden. ### 13.3 Unsynchronized ```cpp synchronization(sync_all_unsynchronized) ``` Properties use the `unsynchronized_slot` and no real Structive mutex protects them. ### 13.4 Per-property override By member pointer: ```cpp sync_independent<&Device::temperature>() sync_unsynchronized<&Device::immutable_id>() ``` Runtime string forms also exist: ```cpp sync_independent("temperature") sync_unsynchronized("immutable_id") ``` Prefer member-pointer rules when the property is statically known. ### 13.5 Groups Member-pointer form: ```cpp sync_group<&Device::min_speed, &Device::max_speed>("speed_range") ``` String form: ```cpp sync_group("speed_range", "min_speed", "max_speed") ``` All properties in a group share one lock slot. The resolver rejects unknown runtime keys, duplicate group names, empty groups and multiple explicit configurations of the same property. ## 14. Per-instance synchronization override The type descriptor supplies a default synchronization plan, but an instance may override it through `Property_Synchronization`: ```cpp struct Device : Property_Object { Device() = default; explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} }; Device shared_device{ property_synchronization(synchronization(sync_all_shared)) }; ``` For schema-aware compile-time synchronization specifications: ```cpp auto policy = property_synchronization( synchronization( sync_all_independent, sync_group<&Device::temperature, &Device::pressure>("environment") ) ); ``` The schema remains the same; only that instance uses an override lock topology. Default instances share one resolved topology per type, so the default plan is not re-resolved for every object. ## 15. Inspect resolved synchronization For a typed member: ```cpp auto slot = device.lock_slot<&Device::temperature>(); ``` For the complete view: ```cpp auto resolved = device.resolved_synchronization(); auto count = resolved.lock_count; auto slot = resolved.slot(0); auto uses_lock = resolved.uses_lock(0); ``` `Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties. ## 16. Multi-property static guards Shared guard: ```cpp auto guard = device.lock_shared<&Device::temperature, &Device::pressure>(); auto temperature = guard.get<&Device::temperature>(); auto pressure = guard.get<&Device::pressure>(); ``` Unique guard: ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); auto old_min = guard.get<&Device::min_speed>(); guard.set<&Device::min_speed>(20.0); guard.set<&Device::max_speed>(120.0); ``` The guard only permits access to properties inside its held synchronization set. Capability views provide equivalent static guards constrained by their access mode: ```cpp auto guard = device.external().lock_shared<&Device::temperature>(); ``` ## 17. Dynamic-key guards When a property set is known only at runtime: ```cpp std::array keys{"temperature", "pressure"}; auto guard = device.lock_shared(keys); ``` or: ```cpp auto guard = device.lock_unique({"min_speed", "max_speed"}); ``` Dynamic guards validate keys and capability visibility at runtime. Lock slots are sorted and deduplicated before acquisition. Read/write through a dynamic guard can still use compile-time members/keys after the guard is acquired; access outside the held set throws `std::logic_error`. ## 18. Traversal Schema traversal: ```cpp schema.for_each_property([](auto index, const auto& property) { // compile-time index and descriptor }); ``` Managed value traversal: ```cpp device.for_each_readable([](auto index, const auto& descriptor, const auto& value) { // one managed read per property }); ``` Locked traversal acquires the complete readable synchronization set first: ```cpp device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) { // all selected readable properties are held under the guard }); ``` For coordinated writable access: ```cpp device.with_all_writable_locked([](auto& guard) { // use guard.get / guard.set }); ``` The persistence view provides `with_all_loadable_locked(...)`. ## 19. Computed property A synchronized computed property receives a read view: ```cpp computed_property([](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); }, key<"speed_span">, external_access) ``` The computed property itself and every dependency read through the view must resolve to the same lock slot. For example: ```cpp synchronization( sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span") ) ``` Computed properties are read-only. ## 20. Trusted accessor properties Core also supports member-function-based trusted access: ```cpp trusted_computed_property<&Device::get_temperature>(key<"temperature">) ``` and getter/setter pairs: ```cpp trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">) ``` These accessors invoke the object member functions directly and mark themselves as trusted object access. They do not use the synchronized computed-view dependency restriction. Use them when the member functions themselves define the correct synchronization/consistency contract. ## 21. Runtime type-erased access Any `Property_Object` is also a `Property_Object_Base`: ```cpp Property_Object_Base& erased = device; ``` Introspection: ```cpp erased.runtime_object_type(); erased.runtime_property_count(); ``` Intrinsic runtime write does not require a mode: ```cpp double value = 35.0; auto result = erased.runtime_write( "temperature", typeid(double), &value ); ``` A boundary projection is selected explicitly when an adapter must respect external or persistence capability metadata: ```cpp auto external_result = erased.runtime_write( Managed_Access_Mode::external, "temperature", typeid(double), &value ); ``` Runtime read uses the same overload model and returns the value through a callback: ```cpp static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { if (type == typeid(double)) { *static_cast(context) = *static_cast(value); } } double output = 0.0; auto result = erased.runtime_read( "temperature", &output, &read_double ); auto external_result = erased.runtime_read( Managed_Access_Mode::external, "temperature", &output, &read_double ); ``` Possible results: ```cpp Runtime_Access_Result::ok Runtime_Access_Result::unknown_property Runtime_Access_Result::not_readable Runtime_Access_Result::not_writable Runtime_Access_Result::type_mismatch ``` The intrinsic runtime overload uses the accessor's intrinsic capability and the same synchronization topology as typed `read()`/`write()`. The overload taking `Managed_Access_Mode` applies the selected external or persistence projection before using the same synchronization rules. ## 22. Lock policy Default: ```cpp struct Device : Property_Object { }; ``` Equivalent shorthand: ```cpp struct Device : Property_Object { }; ``` A no-op lock policy exists: ```cpp struct Device : Property_Object { }; ``` `No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. Its default managed-object path stores no mutex array and performs no per-instance heap allocation for synchronization. A custom `Property_Synchronization` may still allocate a compact override topology because computed-property synchronization-domain checks must preserve the selected per-instance layout. ## 23. Raw object access `unsafe_object()` exposes the derived object directly: ```cpp Device& raw = device.unsafe_object(); ``` Direct field access is also normal C++: ```cpp device.temperature = 40.0; ``` These paths intentionally bypass Structive-managed access. They are useful when the caller already owns the required synchronization or when a field is being handled outside Structive’s managed contract. ## 24. Recommended usage rules - Use member pointers for typed business access. - Use string keys at runtime integration boundaries. - Treat keys as protocol identifiers. - Keep object-wide access policy in `defaults(...)` and override only exceptions. - Use synchronization groups for fields that must be observed consistently. - Validate explicitly at the business operation boundary. - Use static multi-property guards for cross-field invariants. - Prefer synchronized computed properties over trusted accessors. - Use runtime access for adapters, not ordinary typed code. - Use raw access only when the caller intentionally owns the missing managed guarantees.