补全边角

This commit is contained in:
2026-08-08 21:21:23 +08:00
parent a6d6f8c6d3
commit c51c9d2937
24 changed files with 359 additions and 56 deletions
+16 -2
View File
@@ -43,6 +43,7 @@ struct Property_Descriptor {
static constexpr bool writable = Accessor::writable && (intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write);
static constexpr bool synchronized_view_read = Accessor::synchronized_view_read;
static constexpr bool trusted_object_access = Accessor::trusted_object_access;
static constexpr bool runtime_copy_writable = writable && requires(const Accessor& accessor, object_type& object, const value_type& candidate) { accessor.write(object, candidate); };
static_assert(unique_single_value_categories<Attributes...>());
static_assert(!((intrinsic_capability == Property_Capability::read || intrinsic_capability == Property_Capability::read_write) && !Accessor::readable), "property capability requests read from a non-readable accessor");
static_assert(!((intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write) && !Accessor::writable), "property capability requests write from a non-writable accessor");
@@ -55,9 +56,11 @@ struct Property_Descriptor {
template <class Category>
using attribute_type = find_attribute_in_list_t<Category, attribute_types>;
template <class Category>
static constexpr bool has_attribute = !std::same_as<attribute_type<Category>, void>;
static constexpr std::size_t attribute_count = count_attribute_category<Category, Attributes...>();
template <class Category>
constexpr decltype(auto) attribute() const requires has_attribute<Category> {
static constexpr bool has_attribute = attribute_count<Category> != 0;
template <class Category>
constexpr decltype(auto) attribute() const requires (attribute_count<Category> == 1) {
using target = attribute_type<Category>;
return std::get<target>(attributes);
}
@@ -70,6 +73,17 @@ struct Property_Descriptor {
(function(values), ...);
}, attributes);
}
template <class Category, class Function>
constexpr void for_each_attribute(Function&& function) const {
std::apply([&](const auto&... values) {
([&] {
using type = std::remove_cvref_t<decltype(values)>;
if constexpr (std::same_as<attribute_category_of_t<type>, Category>) {
function(values);
}
}(), ...);
}, attributes);
}
template <class Function>
constexpr void for_each_constraint(Function&& function) const {
std::apply([&](const auto&... values) {
@@ -85,6 +85,7 @@ enum class Runtime_Access_Result {
unknown_property,
not_readable,
not_writable,
unsupported_runtime_write,
type_mismatch
};
/// Callback used by `runtime_read`.
@@ -123,8 +124,8 @@ public:
Runtime_Access_Result runtime_read(std::string_view key, void* context, Runtime_Read_Callback callback) const {
return runtime_interface_->read(*this, key, context, callback);
}
/// Looks up `key` at runtime and performs the same managed write used by typed `write`.
/// `value_type` must exactly match the property's declared value type and `value` must point to a live object of that exact type for the duration of the call. No numeric, string or user-defined conversion is attempted.
/// Looks up `key` at runtime and performs a copy-input managed write.
/// `value_type` must exactly match the property's declared value type and `value` must point to a live object of that exact type for the duration of the call. No numeric, string or user-defined conversion is attempted. Intrinsically writable properties that require move-only input report `unsupported_runtime_write`; typed `write` remains available for them.
Runtime_Access_Result runtime_write(std::string_view key, const std::type_info& value_type, const void* value) {
return runtime_interface_->write(*this, key, value_type, value);
}
@@ -656,7 +657,7 @@ private:
return Runtime_Access_Result::not_readable;
}
Runtime_Access_Result result = Runtime_Access_Result::unknown_property;
visit_schema_property(type_descriptor<Derived>(), key, [&](auto property_index_constant, const auto&) {
visit_schema_property_at(type_descriptor<Derived>(), *index, [&](auto property_index_constant, const auto&) {
constexpr std::size_t property_index = decltype(property_index_constant)::value;
using Property = typename Schema::template property_type<property_index>;
using Value = typename Property::value_type;
@@ -707,13 +708,12 @@ private:
return Runtime_Access_Result::not_writable;
}
Runtime_Access_Result result = Runtime_Access_Result::unknown_property;
visit_schema_property(type_descriptor<Derived>(), key, [&](auto property_index_constant, const auto&) {
visit_schema_property_at(type_descriptor<Derived>(), *index, [&](auto property_index_constant, const auto&) {
constexpr std::size_t property_index = decltype(property_index_constant)::value;
using Property = typename Schema::template property_type<property_index>;
using Accessor = typename Property::accessor_type;
using Value = typename Property::value_type;
if constexpr (!Schema::template property_type<property_index>::writable || !requires(const Accessor& accessor, Derived& object, const Value& candidate) { accessor.write(object, candidate); }) {
result = Runtime_Access_Result::not_writable;
if constexpr (!Property::runtime_copy_writable) {
result = Runtime_Access_Result::unsupported_runtime_write;
} else if (value_type != typeid(Value)) {
result = Runtime_Access_Result::type_mismatch;
} else {
+13 -8
View File
@@ -24,7 +24,7 @@ consteval std::string_view declared_property_key() {
}
template <class... Properties>
consteval bool unique_property_keys() {
constexpr std::array keys{declared_property_key<Properties>()...};
constexpr std::array<std::string_view, sizeof...(Properties)> keys{declared_property_key<Properties>()...};
for (std::size_t i = 0; i < keys.size(); ++i) {
for (std::size_t j = i + 1; j < keys.size(); ++j) {
if (keys[i] == keys[j]) {
@@ -374,19 +374,24 @@ Synchronization_Plan materialize_synchronization_plan(Source&& source) {
}
}
template <Valid_Property_Schema Schema, class Function>
bool visit_schema_property(const Schema& schema, std::string_view key_value, Function&& function) {
bool visit_schema_property_at(const Schema& schema, std::size_t index_value, Function&& function) {
bool found = false;
schema.for_each_property([&](auto index, const auto& descriptor) {
if (!found) {
using property_type = std::remove_cvref_t<decltype(descriptor)>;
if (declared_property_key<property_type>() == key_value) {
std::invoke(function, index, descriptor);
found = true;
}
if (!found && decltype(index)::value == index_value) {
std::invoke(function, index, descriptor);
found = true;
}
});
return found;
}
template <Valid_Property_Schema Schema, class Function>
bool visit_schema_property(const Schema& schema, std::string_view key_value, Function&& function) {
auto index = schema_property_index<Schema>(key_value);
if (!index) {
return false;
}
return visit_schema_property_at(schema, *index, std::forward<Function>(function));
}
template <class Object, class Defaults, class Source, class... Properties>
auto make_object_schema(Defaults&& object_defaults, Source&& synchronization_source, Properties&&... properties) requires Synchronization_Source_Type<std::remove_cvref_t<Source>> {
using schema_type = Object_Schema<Object, std::decay_t<Defaults>, std::decay_t<Properties>...>;
+61 -10
View File
@@ -1,13 +1,16 @@
add_library(structive_property_core INTERFACE)
add_library(structive::property_core ALIAS structive_property_core)
target_include_directories(structive_property_core INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include")
set_target_properties(structive_property_core PROPERTIES EXPORT_NAME property_core)
target_include_directories(structive_property_core INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>" "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
target_compile_features(structive_property_core INTERFACE cxx_std_20)
add_executable(structive_property_core_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp")
target_link_libraries(structive_property_core_example PRIVATE structive::property_core)
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(structive_property_core_example PRIVATE -Wall -Wextra -Wpedantic)
endif ()
if (BUILD_TESTING)
if(STRUCTIVE_BUILD_EXAMPLES)
add_executable(structive_property_core_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp")
target_link_libraries(structive_property_core_example PRIVATE structive::property_core)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(structive_property_core_example PRIVATE -Wall -Wextra -Wpedantic)
endif()
endif()
if(STRUCTIVE_BUILD_TESTS)
find_package(Threads REQUIRED)
add_executable(structive_property_core_test "${CMAKE_CURRENT_LIST_DIR}/tests/property_core_test.cpp")
add_executable(structive_property_runtime_api_test "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_api_test.cpp")
@@ -15,12 +18,60 @@ if (BUILD_TESTING)
target_link_libraries(structive_property_core_test PRIVATE structive::property_core Threads::Threads)
target_link_libraries(structive_property_runtime_api_test PRIVATE structive::property_core Threads::Threads)
target_link_libraries(structive_property_synchronization_test PRIVATE structive::property_core Threads::Threads)
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(structive_property_core_test PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(structive_property_runtime_api_test PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(structive_property_synchronization_test PRIVATE -Wall -Wextra -Wpedantic)
endif ()
endif()
add_test(NAME structive_property_core_test COMMAND structive_property_core_test)
add_test(NAME structive_property_runtime_api_test COMMAND structive_property_runtime_api_test)
add_test(NAME structive_property_synchronization_test COMMAND structive_property_synchronization_test)
endif ()
set_tests_properties(structive_property_core_test structive_property_runtime_api_test structive_property_synchronization_test PROPERTIES LABELS "unit;core")
function(structive_add_core_header_test header)
string(REPLACE "/" "_" target_suffix "${header}")
string(REPLACE "." "_" target_suffix "${target_suffix}")
set(target "structive_header_${target_suffix}")
set(source "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests/${target}.cpp")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests")
file(WRITE "${source}" "#include <${header}>\nint main() {\n return 0;\n}\n")
add_executable("${target}" "${source}")
target_link_libraries("${target}" PRIVATE structive::property_core)
add_test(NAME "${target}" COMMAND "${target}")
set_tests_properties("${target}" PROPERTIES LABELS "header;core")
endfunction()
set(structive_core_headers
structive/property/accessor.hpp
structive/property/attributes.hpp
structive/property/descriptor.hpp
structive/property/fixed_string.hpp
structive/property/meta.hpp
structive/property/property.hpp
structive/property/property_object.hpp
structive/property/schema.hpp
structive/property/synchronization.hpp
structive/property/type_descriptor.hpp
structive/property/validation.hpp
)
foreach(header IN LISTS structive_core_headers)
structive_add_core_header_test("${header}")
endforeach()
include(CheckCXXSourceCompiles)
function(structive_expect_compile_failure name source)
file(READ "${source}" source_text)
set(CMAKE_REQUIRED_INCLUDES "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(result_variable "STRUCTIVE_COMPILE_FAIL_${name}")
unset("${result_variable}" CACHE)
check_cxx_source_compiles("${source_text}" "${result_variable}")
if(${result_variable})
message(FATAL_ERROR "Structive compile-fail contract unexpectedly compiled: ${name}")
endif()
endfunction()
structive_expect_compile_failure(duplicate_key "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_key.cpp")
structive_expect_compile_failure(duplicate_storage "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_storage.cpp")
structive_expect_compile_failure(missing_key "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/missing_key.cpp")
structive_expect_compile_failure(capability_mismatch "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/capability_mismatch.cpp")
structive_expect_compile_failure(incompatible_constraint "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/incompatible_constraint.cpp")
structive_expect_compile_failure(foreign_sync_member "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/foreign_sync_member.cpp")
endif()
@@ -0,0 +1,8 @@
#include <structive/property/property.hpp>
struct Capability_Mismatch {
const int value{};
};
int main() {
auto schema = structive::object<Capability_Mismatch>(structive::field<&Capability_Mismatch::value>(structive::key<"value">, structive::read_write));
return static_cast<int>(schema.property_count);
}
@@ -0,0 +1,9 @@
#include <structive/property/property.hpp>
struct Duplicate_Key {
int left{};
int right{};
};
int main() {
auto schema = structive::object<Duplicate_Key>(structive::field<&Duplicate_Key::left>(structive::key<"value">), structive::field<&Duplicate_Key::right>(structive::key<"value">));
return static_cast<int>(schema.property_count);
}
@@ -0,0 +1,8 @@
#include <structive/property/property.hpp>
struct Duplicate_Storage {
int value{};
};
int main() {
auto schema = structive::object<Duplicate_Storage>(structive::field<&Duplicate_Storage::value>(structive::key<"first">), structive::field<&Duplicate_Storage::value>(structive::key<"second">));
return static_cast<int>(schema.property_count);
}
@@ -0,0 +1,11 @@
#include <structive/property/property.hpp>
struct Foreign {
int value{};
};
struct Local {
int value{};
};
int main() {
auto schema = structive::object<Local>(structive::synchronization(structive::sync_independent<&Foreign::value>()), structive::field<&Local::value>(structive::key<"value">));
return static_cast<int>(schema.property_count);
}
@@ -0,0 +1,10 @@
#include <structive/property/property.hpp>
#include <string>
struct Incompatible_Constraint {
int value{};
};
int main() {
auto only_string = structive::constraint<"string_only">([](const std::string&) { return true; });
auto schema = structive::object<Incompatible_Constraint>(structive::field<&Incompatible_Constraint::value>(structive::key<"value">, only_string));
return static_cast<int>(schema.property_count);
}
+8
View File
@@ -0,0 +1,8 @@
#include <structive/property/property.hpp>
struct Missing_Key {
int value{};
};
int main() {
auto schema = structive::object<Missing_Key>(structive::field<&Missing_Key::value>());
return static_cast<int>(schema.property_count);
}
+39 -1
View File
@@ -20,6 +20,16 @@ struct Test_Tag_Attribute {
};
template <int Value>
inline constexpr Test_Tag_Attribute<Value> test_tag{};
struct Test_Multi_Category {};
template <int Value>
struct Test_Multi_Attribute {
using attribute_category = Test_Multi_Category;
static constexpr bool single_valued = false;
static constexpr bool inheritable = false;
static constexpr int value = Value;
};
template <int Value>
inline constexpr Test_Multi_Attribute<Value> test_multi{};
#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false)
struct Device : Property_Object<Device> {
Device() = default;
@@ -35,7 +45,7 @@ struct structive::Type_Descriptor<Device> {
static auto get() {
return object<Device>(
synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")),
field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>),
field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>, test_multi<3>, test_multi<5>),
field<&Device::pressure>(key<"pressure">),
field<&Device::min_speed>(key<"minimum_speed">),
field<&Device::max_speed>(key<"maximum_speed">),
@@ -106,6 +116,13 @@ struct structive::Type_Descriptor<Read_Only_Device> {
);
}
};
struct Empty_Device : Property_Object<Empty_Device> {};
template <>
struct structive::Type_Descriptor<Empty_Device> {
static auto get() {
return object<Empty_Device>();
}
};
struct Pure_Read_Only_Device : Property_Object<Pure_Read_Only_Device> {
int id{21};
int version{4};
@@ -120,6 +137,10 @@ struct structive::Type_Descriptor<Pure_Read_Only_Device> {
);
}
};
template <class Property>
concept Can_Read_Multi_Attribute_As_Single = requires(const Property& property_value) {
property_value.template attribute<Test_Multi_Category>();
};
template <class Object>
concept Can_Write_Immutable = requires(Object& object) {
object.template write<&Device::immutable_id>(1);
@@ -180,7 +201,24 @@ int main() {
REQUIRE(schema.template property<&Device::max_speed>().key() == "maximum_speed");
using Temperature_Property = std::remove_cvref_t<decltype(schema.template property<&Device::temperature>())>;
static_assert(Temperature_Property::template has_attribute<Test_Tag_Category>);
static_assert(Temperature_Property::template attribute_count<Test_Tag_Category> == 1);
static_assert(Temperature_Property::template attribute_count<Test_Multi_Category> == 2);
static_assert(!Can_Read_Multi_Attribute_As_Single<Temperature_Property>);
REQUIRE(Temperature_Property::template attribute_type<Test_Tag_Category>::value == 7);
int multi_sum = 0;
schema.template property<&Device::temperature>().template for_each_attribute<Test_Multi_Category>([&](const auto& attribute) {
multi_sum += std::remove_cvref_t<decltype(attribute)>::value;
});
REQUIRE(multi_sum == 8);
const auto& empty_schema = type_descriptor<Empty_Device>();
static_assert(type_descriptor_schema_t<Empty_Device>::property_count == 0);
REQUIRE(empty_schema.synchronization_plan().property_rules().empty());
Empty_Device empty_device;
REQUIRE(empty_device.resolved_synchronization().lock_count == 0);
Property_Object_Base& empty_erased = empty_device;
REQUIRE(empty_erased.runtime_property_count() == 0);
int empty_value = 0;
REQUIRE(empty_erased.runtime_read("missing", &empty_value, &runtime_read_int) == Runtime_Access_Result::unknown_property);
Device device;
REQUIRE(device.resolved_synchronization().lock_count == 3);
REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot);
+25
View File
@@ -3,6 +3,7 @@
#include <cstdio>
#include <cstdlib>
#include <semaphore>
#include <memory>
#include <string>
#include <thread>
#include <typeinfo>
@@ -24,6 +25,15 @@ struct structive::Type_Descriptor<Runtime_Device> {
);
}
};
struct Move_Only_Runtime_Device : Property_Object<Move_Only_Runtime_Device> {
std::unique_ptr<int> value{std::make_unique<int>(1)};
};
template <>
struct structive::Type_Descriptor<Move_Only_Runtime_Device> {
static auto get() {
return object<Move_Only_Runtime_Device>(field<&Move_Only_Runtime_Device::value>(key<"value">));
}
};
struct Runtime_Read_Capture {
std::size_t calls{};
std::size_t index{};
@@ -82,6 +92,20 @@ static void test_runtime_metadata_and_results() {
REQUIRE(erased.runtime_write("value", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch);
REQUIRE(erased.runtime_write("missing", typeid(int), &value) == Runtime_Access_Result::unknown_property);
}
static void test_runtime_copy_write_boundary() {
using Schema = type_descriptor_schema_t<Move_Only_Runtime_Device>;
using Property = typename Schema::template property_type<0>;
static_assert(Property::writable);
static_assert(!Property::runtime_copy_writable);
Move_Only_Runtime_Device device;
device.write<&Move_Only_Runtime_Device::value>(std::make_unique<int>(9));
REQUIRE(*device.value == 9);
Property_Object_Base& erased = device;
std::unique_ptr<int> replacement = std::make_unique<int>(11);
REQUIRE(erased.runtime_write("value", typeid(std::unique_ptr<int>), &replacement) == Runtime_Access_Result::unsupported_runtime_write);
REQUIRE(*device.value == 9);
REQUIRE(*replacement == 11);
}
static void test_runtime_access_uses_managed_synchronization() {
Runtime_Device device;
Property_Object_Base& erased = device;
@@ -134,6 +158,7 @@ static void test_runtime_read_only_fast_path() {
}
int main() {
test_runtime_metadata_and_results();
test_runtime_copy_write_boundary();
test_runtime_access_uses_managed_synchronization();
test_runtime_read_only_fast_path();
}