From 02a50e1ed4e7a132eae64d01b7dd62af41375b50 Mon Sep 17 00:00:00 2001 From: Kevin Meyer Date: Fri, 28 Aug 2026 11:02:50 +0200 Subject: [PATCH] serialization: add EnumTraits for named enum serialization Add score::common::visitor::EnumTraits trait template plus a new payload_tags::enum_le tag and enum_serialized_descriptor. visit_as SFINAE-dispatches to the enum-aware descriptor when EnumTraits::kHasNames is true, otherwise falls back to the previous integer-only behavior. The wire format (byte layout) is unchanged. This lets downstream code generators (e.g. FIBEX generation for DLT/ CANape tooling) recover enumerator names for proto-derived enums by specializing EnumTraits, without any change for enums that don't register a trait. Also fixes signed-enum detection in the new visit_as overloads: std::is_signed is only meaningful for arithmetic types and is always false for enum types (enums are not arithmetic types per the standard), even when their underlying type is signed. Added details::is_signed_enum, which inspects std::underlying_type_t and is only instantiated for enum types, and use it in place of std::is_signed for the enum dispatch. Added unit test coverage in test_serializer_visitor.cpp: - Verifies EnumTraits::kHasNames defaults to false for enums without a registered trait, and true once specialized. - Verifies the serialized wire size is unchanged for a named enum. - Verifies visit_as dispatches to the enum_le payload tag and exposes the correct enum_type and wire_tag (signed_le for enums with a signed underlying type). - Verifies the registered enumerator name<->value table (kEnumerators) is actually retrievable through the trait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../include/serialization/visit_serialize.h | 131 +++++++++++++++++- .../test/ut/test_serializer_visitor.cpp | 62 +++++++++ 2 files changed, 187 insertions(+), 6 deletions(-) diff --git a/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h b/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h index cfdd225988..b444b6eeef 100644 --- a/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h +++ b/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h @@ -80,11 +80,65 @@ struct vector struct optional { }; +// Distinct tag for enum-typed fields whose enumerator name table is known via a specialization +// of ::score::common::visitor::EnumTraits. Carries the same wire-level integer encoding as +// signed_le / unsigned_le (see enum_serialized_descriptor::wire_tag) but lets downstream +// consumers (e.g. FIBEX generation) detect "this is a named enum" instead of falling through to +// the generic integral overloads. +struct enum_le +{ +}; } // namespace payload_tags +// One value <-> name mapping of an enumerator, as declared in the originating proto `enum`. +struct Enumerator +{ + std::int64_t value; + const char* name; +}; + +// Per-enum-type metadata carrier, used by the serialization visitor and, downstream, by FIBEX +// generation to recover the enumerator name<->value table that would otherwise be lost once the +// enum is folded into a plain integral wire type. +// +// The primary (unspecialized) template intentionally carries no names: any C++ enum works with +// the existing signed/unsigned integral serialization path unless a specialization of this trait +// is provided for it. Code generators are expected to emit a specialization for every proto +// `enum` they generate, reusing the enumerator list they already have in memory during codegen. +// This is a compile-time-only, zero-runtime-cost side channel: it does not alter the wire format +// of the enum itself. +template +struct EnumTraits +{ + static constexpr bool kHasNames = false; +}; + namespace details { +/// \brief Compile-time predicate for "is T an enum with a signed underlying type?". +/// +/// std::is_signed is only meaningful for arithmetic types and is always false for enum types +/// (enums are not arithmetic types per the standard), even when their underlying type is signed. +/// This trait instead inspects the enum's underlying type, and is only instantiated for enum +/// types (via the bool non-type template parameter) so it stays safe to use unconditionally in +/// enable_if_t expressions alongside is_enum for non-enum T. +/// \tparam T Candidate type; only its is_enum-ness is inspected here (this primary/false-type +/// template is selected for all non-enum T). +template ::value> +struct is_signed_enum : std::false_type +{ +}; + +/// \brief Specialization selected for enum types: inherits from +/// std::is_signed>, i.e. ::value is true iff T's underlying +/// integer type is signed. +/// \tparam T Enum type to inspect. +template +struct is_signed_enum : std::is_signed::type> +{ +}; + template auto cast_to_source_serializable_data_span(const T* data, size_t size) -> score::cpp::span { @@ -972,6 +1026,21 @@ struct memcpy_serialized_descriptor using payload_type = memcpy_serialized; }; +// Descriptor for enum-typed fields whose enumerator names are known via EnumTraits. Keeps the +// same on-wire integer representation as memcpy_serialized_descriptor (Tag is signed_le or +// unsigned_le, matching the enum's underlying signedness) but exposes the original enum type so +// that downstream consumers (e.g. the FIBEX generator's visitor) can look up EnumTraits and +// recover the enumerator name<->value table. This does not change the serialized byte layout. +template +// coverity[autosar_cpp14_a11_0_2_violation] +struct enum_serialized_descriptor +{ + using payload_tag = payload_tags::enum_le; + using payload_type = memcpy_serialized; + using wire_tag = Tag; + using enum_type = T; +}; + template ::value) && (std::is_signed::value), std::int32_t> = 0> @@ -1003,9 +1072,34 @@ inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) return memcpy_serialized_descriptor(); } -template ::value) && (std::is_signed::value), std::int32_t> = 0> +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (details::is_signed_enum::value) && (EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches a signed enum with a registered EnumTraits specialization to the +/// enum-aware descriptor. +/// \param T Enum type with a signed underlying type and EnumTraits::kHasNames == true. +/// \return enum_serialized_descriptor, i.e. the same on-wire +/// signed integer representation as memcpy_serialized_descriptor, but additionally +/// exposing T so that EnumTraits can be looked up by downstream consumers. +// This is false positive, Overload signatures are different. +// coverity[autosar_cpp14_m3_2_3_violation : FALSE] +// coverity[autosar_cpp14_a2_10_4_violation : FALSE] +inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) +{ + return enum_serialized_descriptor(); +} + +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (details::is_signed_enum::value) && (!EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches a signed enum without a registered EnumTraits specialization to the +/// plain integral descriptor (previous behavior, unchanged). +/// \param T Enum type with a signed underlying type and EnumTraits::kHasNames == false. +/// \return memcpy_serialized_descriptor. // This is false positive, Overload signatures are different. // coverity[autosar_cpp14_m3_2_3_violation : FALSE] // coverity[autosar_cpp14_a2_10_4_violation : FALSE] @@ -1014,9 +1108,34 @@ inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) return memcpy_serialized_descriptor(); } -template ::value) && (!std::is_signed::value), std::int32_t> = 0> +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (!details::is_signed_enum::value) && (EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches an unsigned enum with a registered EnumTraits specialization to the +/// enum-aware descriptor. +/// \param T Enum type with an unsigned underlying type and EnumTraits::kHasNames == true. +/// \return enum_serialized_descriptor, i.e. the same on-wire +/// unsigned integer representation as memcpy_serialized_descriptor, but additionally +/// exposing T so that EnumTraits can be looked up by downstream consumers. +// This is false positive, Overload signatures are different. +// coverity[autosar_cpp14_m3_2_3_violation : FALSE] +// coverity[autosar_cpp14_a2_10_4_violation : FALSE] +inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) +{ + return enum_serialized_descriptor(); +} + +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (!details::is_signed_enum::value) && (!EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches an unsigned enum without a registered EnumTraits specialization to the +/// plain integral descriptor (previous behavior, unchanged). +/// \param T Enum type with an unsigned underlying type and EnumTraits::kHasNames == false. +/// \return memcpy_serialized_descriptor. // This is false positive, Overload signatures are different. // coverity[autosar_cpp14_m3_2_3_violation : FALSE] // coverity[autosar_cpp14_a2_10_4_violation : FALSE] diff --git a/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp b/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp index 456eab5e3f..2e59855552 100644 --- a/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp +++ b/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp @@ -971,4 +971,66 @@ TEST(clear_functionality_test, test_that_clear_function_can_clear_vector_of_int3 score::common::visitor::detail::clear(vector_wrapper_instance); } +// Enum used to verify that registering ::score::common::visitor::EnumTraits only adds +// compile-time enumerator name metadata and leaves the serialized wire representation unchanged. +enum class EnumWithNames : std::int32_t +{ + kFirst = 0, + kSecond = 1, +}; + } // namespace test + +template <> +struct score::common::visitor::EnumTraits +{ + static constexpr bool kHasNames = true; + // Enumerator name<->value table, as a real code generator would emit it, so the test can + // verify that names are actually retrievable through the specialized trait (not just that + // kHasNames is set). + static constexpr std::array kEnumerators = {{ + {static_cast(test::EnumWithNames::kFirst), "kFirst"}, + {static_cast(test::EnumWithNames::kSecond), "kSecond"}, + }}; +}; + +TEST(serializer_visitor, named_enum_wire_format_unchanged) +{ + RecordProperty("ParentRequirement", "SCR-1633893"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Check that registering EnumTraits for an enum type only adds compile-time " + "enumerator name metadata and does not alter its serialized wire representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "requirements-analysis"); // requirements + + static_assert(::score::common::visitor::EnumTraits::kHasNames, + "EnumTraits::kHasNames must be true once specialized"); + static_assert(!::score::common::visitor::EnumTraits::kHasNames, + "EnumTraits::kHasNames must default to false for enums without a registered trait"); + + // Wire format (serialized size) must stay identical to the plain integral representation. + EXPECT_EQ(check_serialized(), sizeof(test::EnumWithNames)); + + using Descriptor = decltype(::score::common::visitor::visit_as( + std::declval<::score::common::visitor::serialized_visitor&>(), std::declval())); + static_assert( + std::is_same::value, + "an enum with a registered EnumTraits specialization must dispatch to the enum_le payload tag"); + static_assert(std::is_same::value, + "enum_serialized_descriptor must expose the original enum type"); + // test::EnumWithNames has a signed underlying type (std::int32_t): the wire tag must be + // signed_le, not unsigned_le (regression check for is_signed being applied to the enum type + // itself rather than its underlying type). + static_assert(std::is_same::value, + "a named enum with a signed underlying type must use the signed_le wire tag"); + + // The whole point of EnumTraits is to make the enumerator name<->value table retrievable + // through the trait, so verify the actual entries, not just that kHasNames is set. + const auto& enumerators = ::score::common::visitor::EnumTraits::kEnumerators; + ASSERT_EQ(enumerators.size(), 2U); + EXPECT_EQ(enumerators[0].value, static_cast(test::EnumWithNames::kFirst)); + EXPECT_STREQ(enumerators[0].name, "kFirst"); + EXPECT_EQ(enumerators[1].value, static_cast(test::EnumWithNames::kSecond)); + EXPECT_STREQ(enumerators[1].name, "kSecond"); +}