Files
Structive/README.md
T
2026-08-07 17:50:04 +08:00

370 lines
10 KiB
Markdown

# Structive
**Structive enhances ordinary C++ structs with explicit structural metadata and managed property behavior without replacing the native C++ data model.**
[中文文档](README.zh-CN.md) · [Design Philosophy](docs/DESIGN.md) · [Core Guide](docs/CORE_GUIDE.md) · [Extension Guide](docs/EXTENSIONS.md)
## What Structive is
Structive is a C++20 structural property system built around two separate layers:
```text
C++ object model
ordinary members and member functions
├── Type_Descriptor<T> → Object_Schema
│ type-level structure, keys, intrinsic capabilities,
│ attributes, constraints and synchronization description
└── Property_Object<T>
instance-level managed read/write, synchronization,
traversal and type-erased runtime access
```
A type remains ordinary C++:
```cpp
#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)
);
}
};
```
The members are still real members. Structive only adds explicit structural meaning around them.
## Core idea
Structive follows one central rule:
> **Enhance the struct; do not replace the struct.**
That means:
- registered fields remain ordinary C++ members;
- unregistered members remain outside Structive;
- member pointers are preferred compile-time identities;
- string keys exist for dynamic and adapter boundaries;
- metadata does not force storage wrappers such as `Property<T>`;
- raw C++ access remains possible;
- Structive does not try to enforce a security boundary around a public member;
- external systems decide for themselves what they expose or allow;
- Core only describes what the property itself can intrinsically do.
## No access-control subsystem
Structive intentionally has no built-in `internal`, `external`, `persistence`, role, context or policy access modes.
Core does not expose domain-specific access views, permission enums or persistence-specific access modes.
A GUI, RPC service, serializer, plugin system or persistence layer is responsible for deciding which properties it exposes and which operations it permits. Structive does not own that policy.
The Core only answers intrinsic structural questions:
```text
Can this property be read?
Can this property be written?
What is its key?
What metadata and constraints are attached?
What synchronization is required for managed access?
```
## Intrinsic property capability
Every property has one intrinsic capability:
```text
none
read
write
read_write
```
For normal accessors Structive derives this from the accessor automatically. A member field is normally `read_write`; a getter-only computed property is naturally `read`.
The schema can explicitly narrow the capability:
```cpp
field<&Device::serial_number>(
key<"serial_number">,
read_only
)
```
The predefined capability attributes are:
```cpp
read_only
write_only
read_write
inaccessible
```
They are structural contracts, not user permissions.
For a read-only property:
```cpp
auto id = device.read<&Device::serial_number>();
```
is valid, while:
```cpp
device.write<&Device::serial_number>(1002);
```
is unavailable at compile time.
The raw C++ member remains accessible if the C++ type itself makes it accessible:
```cpp
device.serial_number = 1002;
```
That raw write deliberately bypasses the Structive contract and its synchronization guarantees.
## Read-only means no property lock
Property metadata is used for optimization, not only documentation.
A stored property whose intrinsic capability is read-only does not participate in the synchronization topology:
```text
read-only stored property
no lock slot
no mutex contribution
managed read performs no lock lookup
direct accessor read
```
For example:
```cpp
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">)
);
}
};
```
`id` resolves to `unsynchronized_slot`. Only `value` contributes a mutex to the managed object.
This relies on the Structive managed contract. If another thread deliberately writes `device.id` through raw C++ access while a managed read is occurring, that code has bypassed Structive and owns the resulting synchronization responsibility.
## Computed read-only properties
A computed property is usually intrinsically read-only, but it may read writable dependencies.
```cpp
computed_property<Device, int>([](const auto& view) {
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">)
```
The computed value has no writable storage of its own. Its synchronized view protects the consistency domain of writable dependencies. Read-only stored dependencies can be read directly because they cannot change through the managed path.
Writable dependencies that must form one snapshot should be placed in the same synchronization group as the computed property.
## Managed access and raw access
These operations are deliberately different:
```cpp
device.temperature = 30.0;
device.write<&Device::temperature>(30.0);
```
The first is raw C++ access. The second is the managed Structive path.
Managed access provides the behavior described by the schema, including intrinsic capability checks and synchronization. Raw access bypasses that behavior.
Structive follows a cooperative model: it helps correct code express and use structure efficiently; it does not attempt to stop code that intentionally bypasses the system.
## Schema and managed object are separate
`Type_Descriptor<T>` describes the type. `Property_Object<T>` adds per-instance managed behavior.
The default synchronization topology is resolved once per type. Instances do not keep a per-property vector for the default layout. An instance only stores real mutex state required by writable synchronization domains and a compact override layout when that instance explicitly supplies `Property_Synchronization`.
`No_Lock_Policy` removes real mutex storage entirely.
## Unified Attribute model
Core and extensions use one Attribute protocol.
```cpp
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;
};
```
An extension can attach its metadata to the same descriptor:
```cpp
field<&Device::temperature>(
key<"temperature">,
presentation::label<"Temperature">
)
```
Core stores extension metadata but does not interpret extension-owned categories.
## Validation is explicit
Constraints are metadata. `write()` does not automatically execute them.
```cpp
auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0);
```
Validation, transactions, rollback and synchronization are separate concerns.
## Synchronization
Structive provides:
```cpp
sync_all_independent
sync_all_shared
sync_all_unsynchronized
```
and per-property/group rules:
```cpp
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
```
Only properties that require synchronization contribute lock slots. Stored read-only properties are removed from the resolved lock topology even if a broad default rule would otherwise include them.
Multi-property guards deduplicate lock slots and acquire them in stable order:
```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);
```
A typed unique guard can only be requested for intrinsically writable properties.
## Runtime access
`Property_Object_Base` exposes only intrinsic dynamic access:
```cpp
Property_Object_Base& erased = device;
auto type = erased.runtime_object_type();
auto count = erased.runtime_property_count();
```
Runtime operations are key based:
```text
runtime_read(key, ...)
runtime_write(key, type_info, value)
```
They return:
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
There is no runtime access mode. An external adapter decides whether it should call `runtime_read` or `runtime_write` for a given property.
## Current Core metadata
Core currently defines:
- `key<"...">`
- `read_only`
- `write_only`
- `read_write`
- `inaccessible`
- `unit<"...">`
- `sensitive<>`
- `min_value<...>`
- `max_value<...>`
- `finite`
- `constraint<"code">(...)`
`read_only`, `write_only`, `read_write` and `inaccessible` describe the property itself. They are not access-control policies.
## Current extension metadata
The presentation extension defines:
- `presentation::label<"...">`
- `presentation::description<"...">`
- `presentation::group<"...">`
- `presentation::order<N>`
A presentation consumer may independently decide whether a property should be visible or editable. That policy is outside Property Core.
## Build
```cmake
add_subdirectory(path/to/Structive)
target_link_libraries(my_target PRIVATE structive::property_core)
```
For linked extensions:
```cmake
target_link_libraries(my_target PRIVATE structive::property_extensions)
```
Build and test:
```bash
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
## Documentation
- [Design Philosophy and Principles](docs/DESIGN.md)
- [Core Guide](docs/CORE_GUIDE.md)
- [Extension Architecture](docs/EXTENSIONS.md)
- [中文首页](README.zh-CN.md)
- [设计理念与原则(中文)](docs/DESIGN.zh-CN.md)
- [核心使用指南(中文)](docs/CORE_GUIDE.zh-CN.md)
- [扩展体系(中文)](docs/EXTENSIONS.zh-CN.md)