Files
Structive/docs/CORE_GUIDE.md
T
2026-08-07 17:15:02 +08:00

17 KiB
Raw Blame History

Structive Property Core Guide

中文

1. Include and target

Include the complete Core surface with:

#include <structive/property/property.hpp>

CMake target:

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<Derived>:

using namespace structive;
struct Device : Property_Object<Device> {
    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<T> and return an Object_Schema through object<T>(...):

template <>
struct structive::Type_Descriptor<Device> {
    static auto get() {
        return object<Device>(
            defaults(
                external_access<External_Access::read_write>,
                persistence_access<Persistence_Access::load_store>
            ),
            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<Member>(...) is an alias of property<Member>(...) 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:

const auto& schema = type_descriptor<Device>();

For an instance:

Device device;
const auto& schema = device.schema();

Property lookup supports a numeric compile-time index or a member pointer:

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:

auto key_value = temperature.key();

and compile-time traits such as:

using Property = std::remove_cvref_t<decltype(temperature)>;
static_assert(Property::readable);
static_assert(Property::writable);

Category-based Attribute lookup is available through:

static_assert(Property::has_attribute<Unit_Category>);
const auto& attribute = temperature.attribute<Unit_Category>();

All declared Attributes can be traversed:

temperature.for_each_attribute([](const auto& attribute) {
    // inspect attribute type/value
});

Constraints can be traversed separately:

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:

defaults(
    external_access<External_Access::read_write>,
    persistence_access<Persistence_Access::load_store>,
    sensitive<false>
)

A property may override a default:

field<&Device::name>(
    key<"name">,
    external_access<External_Access::read>
)

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<Index, Category>(schema) for extension-owned inheritable categories when either the property or object defaults declare that category.

8. Core Attributes

8.1 Key

key<"temperature">

Required for every property. Non-empty and unique per schema.

8.2 External access

external_access<External_Access::none>
external_access<External_Access::read>
external_access<External_Access::write>
external_access<External_Access::read_write>

This Attribute is inheritable through defaults(...).

8.3 Persistence access

persistence_access<Persistence_Access::none>
persistence_access<Persistence_Access::load>
persistence_access<Persistence_Access::store>
persistence_access<Persistence_Access::load_store>

This Attribute is also inheritable.

8.4 Unit

unit<"C">

This is descriptive metadata and is not inheritable.

8.5 Sensitive

sensitive<>
sensitive<false>

This is inheritable metadata. Core exposes its effective value but does not automatically redact data.

9. Constraints and validation

Built-in constraints:

min_value<0>
max_value<100>
finite

Custom constraint:

constraint<"even">([](int value) {
    return value % 2 == 0;
})

Validation by member pointer:

auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);

Validation by compile-time key:

auto error = validate_property_key_value<"temperature">(device.schema(), candidate);

A failure returns:

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:

auto temperature = device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);

Compile-time key access:

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:

defaults(external_access<External_Access::read_write>)

Use:

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:

device.persistence().load<&Device::temperature>(30.0);
auto value = device.persistence().store<&Device::temperature>();

Compile-time key forms are also available:

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:

synchronization(sync_all_independent)

13.2 Shared

synchronization(sync_all_shared)

All properties use the same lock slot unless overridden.

13.3 Unsynchronized

synchronization(sync_all_unsynchronized)

Properties use the unsynchronized_slot and no real Structive mutex protects them.

13.4 Per-property override

By member pointer:

sync_independent<&Device::temperature>()
sync_unsynchronized<&Device::immutable_id>()

Runtime string forms also exist:

sync_independent("temperature")
sync_unsynchronized("immutable_id")

Prefer member-pointer rules when the property is statically known.

13.5 Groups

Member-pointer form:

sync_group<&Device::min_speed, &Device::max_speed>("speed_range")

String form:

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:

struct Device : Property_Object<Device> {
    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:

auto policy = property_synchronization<Device>(
    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:

auto slot = device.lock_slot<&Device::temperature>();

For the complete view:

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:

auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto temperature = guard.get<&Device::temperature>();
auto pressure = guard.get<&Device::pressure>();

Unique guard:

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:

auto guard = device.external().lock_shared<&Device::temperature>();

17. Dynamic-key guards

When a property set is known only at runtime:

std::array<std::string_view, 2> keys{"temperature", "pressure"};
auto guard = device.lock_shared(keys);

or:

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:

schema.for_each_property([](auto index, const auto& property) {
    // compile-time index and descriptor
});

Managed value traversal:

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:

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:

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:

computed_property<Device, double>([](const auto& view) {
    return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">, external_access<External_Access::read>)

The computed property itself and every dependency read through the view must resolve to the same lock slot.

For example:

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:

trusted_computed_property<&Device::get_temperature>(key<"temperature">)

and getter/setter pairs:

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<T> is also a Property_Object_Base:

Property_Object_Base& erased = device;

Introspection:

erased.runtime_object_type();
erased.runtime_property_count();

Intrinsic runtime write does not require a mode:

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:

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:

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<double*>(context) = *static_cast<const double*>(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:

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:

struct Device : Property_Object<Device, Shared_Mutex_Policy> {
};

Equivalent shorthand:

struct Device : Property_Object<Device> {
};

A no-op lock policy exists:

struct Device : Property_Object<Device, No_Lock_Policy> {
};

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:

Device& raw = device.unsafe_object();

Direct field access is also normal C++:

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 Structives managed contract.

  • 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.