去除所有访问级别

This commit is contained in:
2026-08-07 17:50:04 +08:00
parent 0c7fac70b2
commit 3205dd84e8
17 changed files with 2086 additions and 2424 deletions
+276 -355
View File
@@ -2,227 +2,235 @@
[中文](CORE_GUIDE.zh-CN.md)
## 1. Include and target
Include the complete Core surface with:
## 1. Include and CMake target
```cpp
#include <structive/property/property.hpp>
```
CMake target:
```cmake
target_link_libraries(my_target PRIVATE structive::property_core)
```
The Core target is header-only and requires C++20.
Property Core is C++20.
## 2. Define a managed object
A managed object normally derives from `Property_Object<Derived>`:
```cpp
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"};
int serial_number{1001};
};
```
The members remain ordinary C++ members.
`Property_Object<Device>` adds managed operations. The fields remain ordinary members.
## 3. Define the type descriptor
Specialize `Type_Descriptor<T>` and return an `Object_Schema` through `object<T>(...)`:
```cpp
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")
),
synchronization(sync_all_independent),
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<&Device::pressure>(
key<"pressure">,
unit<"kPa">
),
field<&Device::serial_number>(
key<"serial_number">,
read_only
)
);
}
};
```
`field<Member>(...)` is an alias of `property<Member>(...)` and produces a member-backed `Property_Descriptor`.
The descriptor is the structural definition of `Device`.
## 4. Schema guarantees
The schema enforces several structural conditions:
A valid schema guarantees:
- 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.
- every registered property has a non-empty key;
- keys are unique;
- member selectors belong to the described object type;
- single-valued Attribute categories are not duplicated on the same property;
- capability metadata cannot request operations unsupported by the underlying accessor;
- constraints are compatible with the property value type.
Many schema errors are therefore compile-time errors.
Use:
```cpp
static_assert(Property_Described_Object<Device>);
using Schema = type_descriptor_schema_t<Device>;
static_assert(Valid_Property_Schema<Schema>);
```
## 5. Access the schema
For a described type:
```cpp
const auto& schema = type_descriptor<Device>();
const auto& same_schema = device.schema();
```
For an instance:
```cpp
Device device;
const auto& schema = device.schema();
```
Property lookup supports a numeric compile-time index or a member pointer:
Properties can be selected by index or 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:
A property descriptor exposes compile-time structural facts:
```cpp
auto key_value = temperature.key();
```
and compile-time traits such as:
```cpp
using Property = std::remove_cvref_t<decltype(temperature)>;
using Property = std::remove_cvref_t<decltype(schema.property<&Device::temperature>())>;
static_assert(Property::readable);
static_assert(Property::writable);
using Value = Property::value_type;
using Accessor = Property::accessor_type;
```
Category-based Attribute lookup is available through:
A `read_only` field reports:
```cpp
static_assert(Property::has_attribute<Unit_Category>);
const auto& attribute = temperature.attribute<Unit_Category>();
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
static_assert(Serial::readable);
static_assert(!Serial::writable);
```
All declared Attributes can be traversed:
The key is available at runtime without dynamic allocation:
```cpp
temperature.for_each_attribute([](const auto& attribute) {
// inspect attribute type/value
});
auto key_value = schema.property<&Device::temperature>().key();
```
Constraints can be traversed separately:
## 7. Intrinsic capability
Core capability values are:
```cpp
temperature.for_each_constraint([](const auto& constraint_value) {
// inspect or evaluate a constraint
});
Property_Capability::none
Property_Capability::read
Property_Capability::write
Property_Capability::read_write
```
## 7. Object defaults and effective Attributes
`defaults(...)` provides object-wide values for inheritable Attribute categories:
Convenience Attributes are:
```cpp
defaults(
external_access<External_Access::read_write>,
persistence_access<Persistence_Access::load_store>,
sensitive<false>
)
read_only
write_only
read_write
inaccessible
```
A property may override a default:
The capability belongs to the property itself. It is not an authorization rule.
```cpp
field<&Device::name>(
key<"name">,
external_access<External_Access::read>
)
If capability metadata is absent, Structive derives capability from the accessor.
For a normal non-const member field, the default is read/write.
For a getter-only computed property, the default is read-only.
Capability metadata can narrow the accessor, but it cannot create an operation the accessor does not support.
## 8. No access-control API
Property Core intentionally does not provide access modes or domain views.
There is no built-in distinction between:
```text
internal
external
persistence
```
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.
A consumer decides its own policy. For example, a GUI may choose to expose only selected properties even though all of them are structurally readable.
Extensions can use `declared_effective_attribute<Index, Category>(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category.
Core only exposes intrinsic `readable` and `writable` facts.
## 8. Core Attributes
## 9. Core Attributes
### 8.1 Key
### 9.1 Key
Every property requires one key:
```cpp
key<"temperature">
```
Required for every property. Non-empty and unique per schema.
The key is the structural protocol identifier used by runtime lookup and adapters.
### 8.2 External access
### 9.2 Capability
```cpp
external_access<External_Access::none>
external_access<External_Access::read>
external_access<External_Access::write>
external_access<External_Access::read_write>
read_only
write_only
read_write
inaccessible
```
This Attribute is inheritable through `defaults(...)`.
Capability Attributes are single-valued and non-inheritable.
### 8.3 Persistence access
```cpp
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
### 9.3 Unit
```cpp
unit<"C">
unit<"kPa">
```
This is descriptive metadata and is not inheritable.
Core stores unit metadata but does not perform conversion.
### 8.5 Sensitive
### 9.4 Sensitive
```cpp
sensitive<>
sensitive<false>
```
This is inheritable metadata. Core exposes its effective value but does not automatically redact data.
`sensitive` is inheritable metadata. It does not implement authorization. A consumer may use it as one input to its own policy.
## 9. Constraints and validation
## 10. Custom Attributes and defaults
Built-in constraints:
Any type following the Attribute protocol can be attached to a property.
```cpp
struct Group_Category {};
template <Fixed_String Value>
struct Group_Attribute {
using attribute_category = Group_Category;
static constexpr bool single_valued = true;
static constexpr bool inheritable = true;
static constexpr auto value = Value;
};
```
Inheritable Attributes can be supplied by `defaults(...)`:
```cpp
object<Device>(
defaults(sensitive<>),
field<&Device::temperature>(key<"temperature">)
)
```
Capability is intentionally non-inheritable because each property owns its intrinsic operation set.
## 11. Constraints and validation
Built-in constraints include:
```cpp
min_value<0>
@@ -230,7 +238,7 @@ max_value<100>
finite
```
Custom constraint:
Custom constraints:
```cpp
constraint<"even">([](int value) {
@@ -238,291 +246,241 @@ constraint<"even">([](int value) {
})
```
Validation by member pointer:
Validation is explicit:
```cpp
auto error = validate_property_value<&Device::temperature>(device.schema(), candidate);
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
if (result) {
auto key_value = result->property_key;
auto code = result->code;
}
```
Validation by compile-time key:
`write()` does not automatically call validation.
```cpp
auto error = validate_property_key_value<"temperature">(device.schema(), candidate);
```
## 12. Typed managed read/write
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:
Member-pointer access is the preferred C++ API:
```cpp
auto temperature = device.read<&Device::temperature>();
device.write<&Device::temperature>(30.0);
```
Compile-time key access:
Compile-time key access is also available:
```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:
The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution.
```cpp
defaults(external_access<External_Access::read_write>)
template <class Object>
concept Can_Write_Serial = requires(Object& object) {
object.template write<&Device::serial_number>(1);
};
static_assert(!Can_Write_Serial<Device>);
```
Use:
## 13. Read-only fast path
Stored read-only properties are statically removed from managed locking.
Given:
```cpp
device.external().write<&Device::temperature>(30.0);
auto value = device.external().read<&Device::temperature>();
field<&Device::serial_number>(key<"serial_number">, read_only)
```
A const object produces a const capability view and therefore has no write API.
Structive resolves:
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>();
```text
serial_number -> unsynchronized_slot
```
Compile-time key forms are also available:
and `read<&Device::serial_number>()` does not query a lock slot or construct a `shared_lock`.
```cpp
device.persistence().load_key<"temperature">(30.0);
auto value = device.persistence().store_key<"temperature">();
```
This remains true even when the broad default is `sync_all_shared`.
The persistence view is controlled by `Persistence_Access` metadata.
If an object has only stored read-only properties, those properties contribute zero mutexes to `resolved_synchronization().lock_count`.
## 13. Synchronization plans
## 14. Synchronization plans
### 13.1 Default independent
The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain.
Explicit form:
### 14.1 Independent
```cpp
synchronization(sync_all_independent)
```
### 13.2 Shared
Each property that actually requires synchronization gets its own lock domain.
### 14.2 Shared
```cpp
synchronization(sync_all_shared)
```
All properties use the same lock slot unless overridden.
All properties that require synchronization share one lock domain.
### 13.3 Unsynchronized
Read-only stored properties are excluded before lock slots are materialized.
### 14.3 Unsynchronized
```cpp
synchronization(sync_all_unsynchronized)
```
Properties use the `unsynchronized_slot` and no real Structive mutex protects them.
Managed access performs no real locking even for writable properties.
### 13.4 Per-property override
By member pointer:
### 14.4 Per-property override
```cpp
sync_independent<&Device::temperature>()
sync_unsynchronized<&Device::immutable_id>()
synchronization(
sync_all_independent,
sync_unsynchronized<&Device::temperature>()
)
```
Runtime string forms also exist:
Dynamic-key rule forms are also available.
### 14.5 Groups
```cpp
sync_independent("temperature")
sync_unsynchronized("immutable_id")
synchronization(
sync_all_independent,
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
)
```
Prefer member-pointer rules when the property is statically known.
Members in one group resolve to the same lock slot if they require synchronization.
### 13.5 Groups
## 15. Per-instance synchronization override
Member-pointer form:
An object can explicitly override the type default:
```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> {
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<Device>(
synchronization(
sync_all_independent,
sync_group<&Device::temperature, &Device::pressure>("environment")
Device device{
property_synchronization(
synchronization(sync_all_shared)
)
);
};
```
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.
For typed member rules:
## 15. Inspect resolved synchronization
```cpp
Device device{
property_synchronization<Device>(
synchronization(
sync_all_independent,
sync_group<&Device::temperature, &Device::pressure>("environment")
)
)
};
```
For a typed member:
The default topology is shared per type. Only an object with an explicit override stores its compact override layout.
## 16. Inspect resolved synchronization
```cpp
auto view = device.resolved_synchronization();
auto count = view.lock_count;
```
Inspect a member slot:
```cpp
auto slot = device.lock_slot<&Device::temperature>();
```
For the complete view:
No-lock properties use:
```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
```
`Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties.
For a read-only stored property this is automatic.
## 16. Multi-property static guards
## 17. Static multi-property guards
Shared guard:
Read 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:
Write 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);
guard.set<&Device::min_speed>(20);
guard.set<&Device::max_speed>(120);
```
The guard only permits access to properties inside its held synchronization set.
Static guards deduplicate slots and acquire them in stable slot order.
Capability views provide equivalent static guards constrained by their access mode:
A typed unique guard requires all selected properties to be intrinsically writable.
## 18. Dynamic-key guards
```cpp
auto guard = device.external().lock_shared<&Device::temperature>();
auto read_guard = device.lock_shared({"temperature", "pressure"});
auto write_guard = device.lock_unique({"temperature", "pressure"});
```
## 17. Dynamic-key guards
Unknown keys throw `std::invalid_argument`.
When a property set is known only at runtime:
A dynamic unique guard rejects a read-only property because the key is only known at runtime.
## 19. Traversal
Schema-only traversal:
```cpp
std::array<std::string_view, 2> 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
schema.for_each_property([&](auto index, const auto& descriptor) {
});
```
Managed value traversal:
Readable value traversal:
```cpp
device.for_each_readable([](auto index, const auto& descriptor, const auto& value) {
// one managed read per property
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
});
```
Locked traversal acquires the complete readable synchronization set first:
Locked readable traversal:
```cpp
device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) {
// all selected readable properties are held under the guard
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
});
```
For coordinated writable access:
The locked traversal acquires only actual synchronization slots. Read-only stored properties do not introduce locks.
Writable transaction-style guard acquisition:
```cpp
device.with_all_writable_locked([](auto& guard) {
// use guard.get / guard.set
device.with_all_writable_locked([&](auto& guard) {
});
```
The persistence view provides `with_all_loadable_locked(...)`.
This acquires all writable managed lock domains; it does not perform validation or rollback automatically.
## 19. Computed property
## 20. Computed properties
A synchronized computed property receives a read view:
```cpp
computed_property<Device, double>([](const auto& view) {
computed_property<Device, int>([](const auto& view) {
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
}, key<"speed_span">, external_access<External_Access::read>)
}, key<"speed_span">)
```
The computed property itself and every dependency read through the view must resolve to the same lock slot.
For example:
Writable dependencies that must form one snapshot should share the computed synchronization domain:
```cpp
synchronization(
@@ -531,27 +489,27 @@ synchronization(
)
```
Computed properties are read-only.
A read-only stored dependency may be read through the computed view without joining a lock slot, because it has no managed writer.
## 20. Trusted accessor properties
## 21. Trusted accessor properties
Core also supports member-function-based trusted access:
Trusted getter:
```cpp
trusted_computed_property<&Device::get_temperature>(key<"temperature">)
trusted_computed_property<&Device::value>(key<"value">)
```
and getter/setter pairs:
Trusted getter/setter:
```cpp
trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">)
trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
```
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.
These forms access the object directly rather than using synchronized dependency views. They should only be used when the caller understands the synchronization contract of those member functions.
## 21. Runtime type-erased access
## 22. Runtime type-erased access
Any `Property_Object<T>` is also a `Property_Object_Base`:
Use `Property_Object_Base` for dynamic adapters:
```cpp
Property_Object_Base& erased = device;
@@ -564,112 +522,75 @@ erased.runtime_object_type();
erased.runtime_property_count();
```
Intrinsic runtime write does not require a mode:
Read:
```cpp
double value = 35.0;
auto result = erased.runtime_write(
"temperature",
typeid(double),
&value
);
auto result = erased.runtime_read("temperature", context, callback);
```
A boundary projection is selected explicitly when an adapter must respect external or persistence capability metadata:
Write:
```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<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
);
auto result = erased.runtime_write("temperature", typeid(double), &value);
```
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
```text
ok
unknown_property
not_readable
not_writable
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.
Runtime access has no external/persistence mode. An adapter owns its own policy and chooses whether it calls the intrinsic read/write operation.
## 22. Lock policy
Runtime read of a stored read-only property follows the same no-lock fast path as typed read.
## 23. Lock policies
Default:
```cpp
struct Device : Property_Object<Device, Shared_Mutex_Policy> {
};
Property_Object<Device, Shared_Mutex_Policy>
```
Equivalent shorthand:
No-lock:
```cpp
struct Device : Property_Object<Device> {
};
Property_Object<Device, No_Lock_Policy>
```
A no-op lock policy exists:
`No_Lock_Policy` stores no real mutex array and avoids real lock objects on managed hot paths.
```cpp
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:
## 24. Raw object access
```cpp
Device& raw = device.unsafe_object();
```
Direct field access is also normal C++:
or normal public member access:
```cpp
device.temperature = 40.0;
device.temperature = 30.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.
Both bypass the Structive managed contract.
## 24. Recommended usage rules
This is intentional. Structive is cooperative structural infrastructure, not forced encapsulation.
- 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.
## 25. Recommended usage rules
Use these defaults:
1. Use ordinary C++ members for storage.
2. Register only fields that belong to the structural model.
3. Prefer member-pointer typed APIs in C++ business code.
4. Use `read_only` when the Structive managed model must never write a stored property.
5. Rely on the resulting zero-lock optimization for stored read-only data.
6. Keep raw writes to read-only properties outside concurrent managed code.
7. Use synchronization groups for mutable cross-field consistency.
8. Keep validation explicit.
9. Use runtime access only for genuinely dynamic adapters.
10. Let GUI/RPC/persistence/authorization layers own their own exposure and access policy.