首次提交

This commit is contained in:
2026-08-07 16:21:44 +08:00
parent 396311246c
commit 1e903dfe1e
33 changed files with 5960 additions and 1 deletions
+327 -1
View File
@@ -1,3 +1,329 @@
# Structive
增强版c++ struct
**Structive enhances ordinary C++ structs with an explicit structural metadata and managed-property layer without replacing their native 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 property and structural-description system built around two deliberately separate layers:
```text
C++ object model
ordinary members, member functions, native layout and direct access
├── Type_Descriptor<T> → Object_Schema
│ compile-time structure, keys, attributes, constraints, access capabilities
└── Property_Object<T>
per-instance managed access, synchronization, traversal and runtime access
```
A type can remain recognizably ordinary C++:
```cpp
#include <structive/property/property.hpp>
using namespace structive;
struct Device : Property_Object<Device> {
double temperature{25.0};
double pressure{101.3};
};
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>
),
field<&Device::temperature>(
key<"temperature">,
unit<"C">,
min_value<-50.0>,
max_value<200.0>
),
field<&Device::pressure>(
key<"pressure">,
unit<"kPa">
)
);
}
};
```
The members are still real members. Structive adds a second, explicit layer that generic systems can understand.
## Core idea
Structive follows one central rule:
> **Enhance the struct; do not replace the struct.**
This has several consequences:
- A registered field remains an ordinary C++ member.
- Unregistered members remain completely outside the property system.
- Member pointers are the preferred compile-time identity for business code.
- String keys exist for runtime and adapter boundaries.
- Metadata does not force storage wrappers such as `Property<T>` into every field.
- Validation, synchronization, persistence capability and presentation metadata remain separate concerns.
The result is intended to let the same C++ type participate in UI, persistence, serialization, RPC or tooling layers without making the core object depend on those systems.
## Project layers
```text
core/
└── structive::property_core
├── INTERFACE target
├── Type_Descriptor<T> and Object_Schema
├── member/computed accessors
├── unified Attribute protocol
├── constraints and explicit validation
├── access capability metadata
├── synchronization plans and guards
├── typed and runtime access
└── traversal
extensions/
└── structive::property_extensions
├── STATIC target
├── depends on property_core
├── defines extension-owned Attribute categories
└── currently provides presentation metadata interpretation
```
The dependency direction is one-way: **extensions depend on core; core never includes or links extensions.**
## Why not `Property<T>` members?
Structive intentionally does not require this:
```cpp
struct Device {
Property<double> temperature;
};
```
Instead, the storage stays native:
```cpp
struct Device : Property_Object<Device> {
double temperature;
};
```
and the structural meaning is declared separately with `Type_Descriptor<Device>`.
This preserves normal C++ member semantics, keeps raw object access available when it is intentionally needed, and lets Structive remain an enhancement layer rather than a replacement object model.
## Schema and managed object are different concepts
`Type_Descriptor<T>` describes the **type**. `Property_Object<T>` adds state and behavior to an **instance**.
The schema contains the registered property tuple, object defaults and the default synchronization plan. A `Property_Object<T>` resolves that plan for each instance and owns its lock topology and mutex storage.
This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state.
## Unified Attribute model
There is one Attribute protocol. Core metadata and extension metadata use the same mechanism.
A single-valued Attribute category can be defined as:
```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;
};
```
Then it can be attached directly to a property:
```cpp
field<&Device::temperature>(
key<"temperature">,
presentation::label<"Temperature">
)
```
Core stores and traverses the Attribute but does not interpret categories it does not own. The owning extension interprets its own category.
This is the primary extension boundary of Structive.
## Managed access is not forced encapsulation
Both of these are valid but mean different things:
```cpp
device.temperature = 30.0;
device.write<&Device::temperature>(30.0);
```
The first is the **raw C++ path**. It bypasses Structive-managed synchronization, access capabilities and other managed behavior.
The second is the **managed path**. It resolves the registered property and uses the configured synchronization behavior.
Structive intentionally keeps both. A codebase should choose the appropriate path according to its ownership and concurrency rules.
## Access capability views
Core defines three managed access modes:
- `internal`: normal managed access from application code.
- `external`: controlled by `External_Access` metadata.
- `persistence`: controlled by `Persistence_Access` metadata.
Example:
```cpp
device.write<&Device::temperature>(30.0);
device.external().write<&Device::temperature>(31.0);
device.persistence().load<&Device::temperature>(32.0);
auto stored = device.persistence().store<&Device::temperature>();
```
Capability semantics are checked by the schema: a property cannot advertise readable/writable capabilities that its accessor does not actually support.
## Validation is explicit
Constraints are metadata. `write()` does **not** automatically execute them.
```cpp
auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0);
if (error) {
// error->property_key
// error->code
}
```
This is intentional. Field validation, cross-field invariants, transactions and rollback are different concerns and should not be hidden inside a generic setter.
## Synchronization is explicit and composable
The default modes are:
```cpp
sync_all_independent
sync_all_shared
sync_all_unsynchronized
```
Properties may then be overridden or grouped:
```cpp
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
```
A group means those properties resolve to the same lock slot. 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.0);
```
Synchronization does not imply validation, transaction, rollback or event emission.
## Computed properties
Structive supports synchronized computed properties:
```cpp
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>)
```
A synchronized computed property reads dependencies through the synchronization view. Those dependencies must resolve to the same synchronization slot as the computed property. This makes the dependency relationship explicit in the synchronization plan.
Advanced trusted accessor forms also exist for member-function getters and getter/setter pairs. They intentionally operate through trusted object access and therefore should be used only when their synchronization semantics are understood by the caller.
## Runtime access
`Property_Object_Base` provides type-erased runtime access for adapter-style code:
```cpp
Property_Object_Base& erased = device;
auto type = erased.runtime_object_type();
auto count = erased.runtime_property_count();
```
Runtime reads and writes use property keys, access modes and `std::type_info`, and report one of:
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
This path is intended for boundaries such as generic serialization, HTTP/RPC adapters, scripting bridges or tooling. Compile-time business code should generally prefer member pointers.
## Current core-owned metadata
The current core defines:
- `key<"...">`
- `external_access<...>`
- `persistence_access<...>`
- `unit<"...">`
- `sensitive<>`
- `min_value<...>`
- `max_value<...>`
- `finite`
- `constraint<"code">(...)`
`external_access`, `persistence_access` and `sensitive` are inheritable and can be supplied through `defaults(...)`. Property-level declarations override object defaults for the same category.
## Current extension metadata
The presentation extension currently defines:
- `presentation::label<"...">`
- `presentation::description<"...">`
- `presentation::group<"...">`
- `presentation::order<N>`
`presentation::describe()` interprets those attributes and falls back to the property key when no label is declared. That fallback belongs to the presentation extension, not to Property Core.
## Build
Structive currently exposes CMake targets for use through the project tree:
```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 the repository with:
```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)