去除所有访问级别
This commit is contained in:
@@ -1,57 +1,47 @@
|
||||
# Structive
|
||||
|
||||
**Structive enhances ordinary C++ structs with an explicit structural metadata and managed-property layer without replacing their native data model.**
|
||||
**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 property and structural-description system built around two deliberately separate layers:
|
||||
Structive is a C++20 structural property system built around two separate layers:
|
||||
|
||||
```text
|
||||
C++ object model
|
||||
ordinary members, member functions, native layout and direct access
|
||||
ordinary members and member functions
|
||||
│
|
||||
├── Type_Descriptor<T> → Object_Schema
|
||||
│ compile-time structure, keys, attributes, constraints, access capabilities
|
||||
│ type-level structure, keys, intrinsic capabilities,
|
||||
│ attributes, constraints and synchronization description
|
||||
│
|
||||
└── Property_Object<T>
|
||||
per-instance managed access, synchronization, traversal and runtime access
|
||||
instance-level managed read/write, synchronization,
|
||||
traversal and type-erased runtime access
|
||||
```
|
||||
|
||||
A type can remain recognizably ordinary C++:
|
||||
A type remains ordinary C++:
|
||||
|
||||
```cpp
|
||||
#include <structive/property/property.hpp>
|
||||
using namespace structive;
|
||||
struct Device : Property_Object<Device> {
|
||||
double temperature{25.0};
|
||||
double pressure{101.3};
|
||||
int serial_number{1001};
|
||||
};
|
||||
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">
|
||||
)
|
||||
field<&Device::temperature>(key<"temperature">, unit<"C">),
|
||||
field<&Device::serial_number>(key<"serial_number">, read_only)
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The members are still real members. Structive adds a second, explicit layer that generic systems can understand.
|
||||
The members are still real members. Structive only adds explicit structural meaning around them.
|
||||
|
||||
## Core idea
|
||||
|
||||
@@ -59,77 +49,172 @@ Structive follows one central rule:
|
||||
|
||||
> **Enhance the struct; do not replace the struct.**
|
||||
|
||||
This has several consequences:
|
||||
That means:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
## No access-control subsystem
|
||||
|
||||
## Project layers
|
||||
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
|
||||
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
|
||||
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?
|
||||
```
|
||||
|
||||
The dependency direction is one-way: **extensions depend on core; core never includes or links extensions.**
|
||||
## Intrinsic property capability
|
||||
|
||||
## Why not `Property<T>` members?
|
||||
Every property has one intrinsic capability:
|
||||
|
||||
Structive intentionally does not require this:
|
||||
```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
|
||||
struct Device {
|
||||
Property<double> temperature;
|
||||
};
|
||||
field<&Device::serial_number>(
|
||||
key<"serial_number">,
|
||||
read_only
|
||||
)
|
||||
```
|
||||
|
||||
Instead, the storage stays native:
|
||||
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> {
|
||||
double temperature;
|
||||
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">)
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
and the structural meaning is declared separately with `Type_Descriptor<Device>`.
|
||||
`id` resolves to `unsynchronized_slot`. Only `value` contributes a mutex to the managed object.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Schema and managed object are different concepts
|
||||
## Computed read-only properties
|
||||
|
||||
`Type_Descriptor<T>` describes the **type**. `Property_Object<T>` adds state and behavior to an **instance**.
|
||||
A computed property is usually intrinsically read-only, but it may read writable dependencies.
|
||||
|
||||
The schema contains the registered property tuple, object defaults and the default synchronization plan. `Property_Object<T>` shares one resolved default lock topology per type and keeps only instance synchronization state that is actually required: real mutex storage for locking policies and a compact override layout only when an instance explicitly supplies `Property_Synchronization`.
|
||||
```cpp
|
||||
computed_property<Device, int>([](const auto& view) {
|
||||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||||
}, key<"speed_span">)
|
||||
```
|
||||
|
||||
This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state.
|
||||
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
|
||||
|
||||
There is one Attribute protocol. Core metadata and extension metadata use the same mechanism.
|
||||
|
||||
A single-valued Attribute category can be defined as:
|
||||
Core and extensions use one Attribute protocol.
|
||||
|
||||
```cpp
|
||||
struct Label_Category {};
|
||||
@@ -142,7 +227,7 @@ struct Label_Attribute {
|
||||
};
|
||||
```
|
||||
|
||||
Then it can be attached directly to a property:
|
||||
An extension can attach its metadata to the same descriptor:
|
||||
|
||||
```cpp
|
||||
field<&Device::temperature>(
|
||||
@@ -151,69 +236,21 @@ field<&Device::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.
|
||||
|
||||
## Intrinsic capability and boundary projections
|
||||
|
||||
A property first has an **intrinsic capability** derived from its accessor:
|
||||
|
||||
- readable if the accessor can read it;
|
||||
- writable if the accessor can write it.
|
||||
|
||||
Normal managed application code uses that intrinsic capability directly:
|
||||
|
||||
```cpp
|
||||
device.write<&Device::temperature>(30.0);
|
||||
auto value = device.read<&Device::temperature>();
|
||||
```
|
||||
|
||||
Core then defines two boundary projections over the same property definition:
|
||||
|
||||
- `external`: controlled by `External_Access` metadata;
|
||||
- `persistence`: controlled by `Persistence_Access` metadata.
|
||||
|
||||
```cpp
|
||||
device.external().write<&Device::temperature>(31.0);
|
||||
device.persistence().load<&Device::temperature>(32.0);
|
||||
auto stored = device.persistence().store<&Device::temperature>();
|
||||
```
|
||||
|
||||
There is no separate `internal` capability mode. The default managed API is the intrinsic property capability itself. `Managed_Access_Mode` therefore identifies only boundary projections. A projection can only narrow abilities that the accessor actually provides; it cannot make an intrinsically unreadable or unwritable property readable or writable.
|
||||
Core stores extension metadata but does not interpret extension-owned categories.
|
||||
|
||||
## Validation is explicit
|
||||
|
||||
Constraints are metadata. `write()` does **not** automatically execute them.
|
||||
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.
|
||||
Validation, transactions, rollback and synchronization are separate concerns.
|
||||
|
||||
## Synchronization is explicit and composable
|
||||
## Synchronization
|
||||
|
||||
The default modes are:
|
||||
Structive provides:
|
||||
|
||||
```cpp
|
||||
sync_all_independent
|
||||
@@ -221,7 +258,7 @@ sync_all_shared
|
||||
sync_all_unsynchronized
|
||||
```
|
||||
|
||||
Properties may then be overridden or grouped:
|
||||
and per-property/group rules:
|
||||
|
||||
```cpp
|
||||
synchronization(
|
||||
@@ -230,33 +267,21 @@ synchronization(
|
||||
)
|
||||
```
|
||||
|
||||
A group means those properties resolve to the same lock slot. Multi-property guards deduplicate lock slots and acquire them in stable order.
|
||||
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.0);
|
||||
guard.set<&Device::min_speed>(20);
|
||||
```
|
||||
|
||||
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.
|
||||
A typed unique guard can only be requested for intrinsically writable properties.
|
||||
|
||||
## Runtime access
|
||||
|
||||
`Property_Object_Base` provides type-erased runtime access for adapter-style code:
|
||||
`Property_Object_Base` exposes only intrinsic dynamic access:
|
||||
|
||||
```cpp
|
||||
Property_Object_Base& erased = device;
|
||||
@@ -264,7 +289,14 @@ 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:
|
||||
Runtime operations are key based:
|
||||
|
||||
```text
|
||||
runtime_read(key, ...)
|
||||
runtime_write(key, type_info, value)
|
||||
```
|
||||
|
||||
They return:
|
||||
|
||||
```text
|
||||
ok
|
||||
@@ -274,15 +306,17 @@ 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.
|
||||
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-owned metadata
|
||||
## Current Core metadata
|
||||
|
||||
The current core defines:
|
||||
Core currently defines:
|
||||
|
||||
- `key<"...">`
|
||||
- `external_access<...>`
|
||||
- `persistence_access<...>`
|
||||
- `read_only`
|
||||
- `write_only`
|
||||
- `read_write`
|
||||
- `inaccessible`
|
||||
- `unit<"...">`
|
||||
- `sensitive<>`
|
||||
- `min_value<...>`
|
||||
@@ -290,23 +324,21 @@ The current core defines:
|
||||
- `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.
|
||||
`read_only`, `write_only`, `read_write` and `inaccessible` describe the property itself. They are not access-control policies.
|
||||
|
||||
## Current extension metadata
|
||||
|
||||
The presentation extension currently defines:
|
||||
The presentation extension 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.
|
||||
A presentation consumer may independently decide whether a property should be visible or editable. That policy is outside 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)
|
||||
@@ -318,7 +350,7 @@ For linked extensions:
|
||||
target_link_libraries(my_target PRIVATE structive::property_extensions)
|
||||
```
|
||||
|
||||
Build and test the repository with:
|
||||
Build and test:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -DBUILD_TESTING=ON
|
||||
|
||||
Reference in New Issue
Block a user