diff --git a/README.md b/README.md index 3e9c9f9..465b59c 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,39 @@ auto [data, in] = zpp::bits::data_in(); auto [data, out] = zpp::bits::data_out(); ``` + +* If your object and its subobjects recursively can just be byte copied, i.e don't have references or pointers or non +trivially copyable subobjects (more accurately, your object fits into the bit cast constexpr requirements), then you don't need to specify the number of members and +serializing your object becomes a `memcpy` operation. Unless of course you define an explicit serialization function in which +case the members are serialized one by one separately. +```cpp +struct point +{ + int x; + int y; +}; + +auto [data, out] = zpp::bits::data_out(); +out(point{1337, 1338}); +``` +The above is serializable/deserializable without any modification by using a `memcpy` operation. +This has the advantage of better performance most of the times, but the disadvantage is that the format becomes less portable +due to potential padding bytes. + +To avoid this behavior and serialize members one by one you can either define the explicit serialization function or +use `zpp::bits::explicit_members` and provide the number of members: +```cpp +struct requires_padding +{ + using serialize = zpp::bits::explicit_members<2>; + + std::byte x; + int y; +}; +``` +If you are using automatic member detection as per `ZPP_BITS_AUTODETECT_MEMBERS_MODE=1`, you may leave the +angle brackets empty as in `zpp::bits::explicit_members<>`. + * Archives can be constructed from either one of the byte types: ```cpp // Either one of these work with the below. diff --git a/test/src/test_byte_serializable.cpp b/test/src/test_byte_serializable.cpp new file mode 100644 index 0000000..88c2cb9 --- /dev/null +++ b/test/src/test_byte_serializable.cpp @@ -0,0 +1,201 @@ +#include "test.h" +#include + +namespace test_byte_serializable +{ + +struct integers +{ + int x; + int y; +}; + +struct explicit_integers +{ + using serialize = zpp::bits::explicit_members<2>; + int x; + int y; +}; + +struct explicit_integers_auto +{ + using serialize = zpp::bits::explicit_members<>; + int x; + int y; +}; + +struct ref +{ + int & x; + int y; +}; + +struct pointer +{ + int * x; + int y; +}; + +struct array +{ + int x[5]; + int y; +}; + +struct stdarray +{ + std::array x; + int y; +}; + +struct stdarray_array +{ + std::array x[5]; + int y; +}; + +struct vector +{ + std::vector x; + int y; +}; + +static_assert(zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert( + !zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert(zpp::bits::concepts::byte_serializable); +static_assert(zpp::bits::concepts::byte_serializable); +static_assert(zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable>); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable); +static_assert(!zpp::bits::concepts::byte_serializable>); +static_assert(zpp::bits::concepts::byte_serializable>); + +class inaccessible +{ +public: + inaccessible() = default; + inaccessible(int x, int y) : x(x), y(y) + { + } + + std::strong_ordering operator<=>(const inaccessible & other) const = + default; + +public: + int x{}; + int y{}; +}; + +TEST(byte_serializable, inaccessible) +{ + static_assert(zpp::bits::concepts::byte_serializable); + + auto [data, in, out] = zpp::bits::data_in_out(); + out(inaccessible{1337, 1338}).or_throw(); + + inaccessible s; + in(s).or_throw(); + + EXPECT_EQ(s, (inaccessible{1337, 1338})); +} + +struct requires_padding +{ + std::byte b{}; + std::int32_t i32{}; +}; + +TEST(byte_serializable, requires_padding) +{ + static_assert( + zpp::bits::concepts::byte_serializable); + static_assert(sizeof(requires_padding) > 5); + + auto [data, in, out] = zpp::bits::data_in_out(); + out(requires_padding{std::byte{0x25}, 0x1337}).or_throw(); + + EXPECT_EQ(data.size(), sizeof(requires_padding)); + EXPECT_NE(hexlify(data), + "25" + "37130000"); + + requires_padding s; + in(s).or_throw(); + EXPECT_EQ(in.position(), sizeof(requires_padding)); + EXPECT_EQ(s.b, std::byte{0x25}); + EXPECT_EQ(s.i32, 0x1337); +} + +struct explicit_requires_padding +{ + using serialize = zpp::bits::explicit_members<2>; + std::byte b{}; + std::int32_t i32{}; +}; + +TEST(byte_serializable, explicit_requires_padding) +{ + static_assert(!zpp::bits::concepts::byte_serializable< + explicit_requires_padding>); + static_assert(sizeof(requires_padding) == + sizeof(explicit_requires_padding)); + static_assert(sizeof(explicit_requires_padding) > 5); + + auto [data, in, out] = zpp::bits::data_in_out(); + out(explicit_requires_padding{std::byte{0x25}, 0x1337}).or_throw(); + + EXPECT_EQ(data.size(), std::size_t{5}); + EXPECT_EQ(hexlify(data), + "25" + "37130000"); + + explicit_requires_padding s; + in(s).or_throw(); + EXPECT_EQ(in.position(), std::size_t{5}); + EXPECT_EQ(s.b, std::byte{0x25}); + EXPECT_EQ(s.i32, 0x1337); +} + +#if ZPP_BITS_AUTODETECT_MEMBERS_MODE > 0 +struct explicit_requires_padding_auto +{ + using serialize = zpp::bits::explicit_members<>; + std::byte b{}; + std::int32_t i32{}; +}; + +static_assert(sizeof(requires_padding) == + sizeof(explicit_requires_padding_auto)); + +TEST(byte_serializable, explicit_requires_padding_auto) +{ + static_assert(!zpp::bits::concepts::byte_serializable< + explicit_requires_padding_auto>); + static_assert(sizeof(requires_padding) == + sizeof(explicit_requires_padding_auto)); + static_assert(sizeof(explicit_requires_padding_auto) > 5); + + auto [data, in, out] = zpp::bits::data_in_out(); + out(explicit_requires_padding_auto{std::byte{0x25}, 0x1337}) + .or_throw(); + + EXPECT_EQ(data.size(), std::size_t{5}); + EXPECT_EQ(hexlify(data), + "25" + "37130000"); + + explicit_requires_padding_auto s; + in(s).or_throw(); + EXPECT_EQ(in.position(), std::size_t{5}); + EXPECT_EQ(s.b, std::byte{0x25}); + EXPECT_EQ(s.i32, 0x1337); +} +#endif + +} // namespace test_byte_serializable diff --git a/zpp_bits.h b/zpp_bits.h index 82e119f..7f511ce 100644 --- a/zpp_bits.h +++ b/zpp_bits.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,18 @@ constexpr ToType bit_cast(FromType const & from) noexcept } // namespace std #endif +template +struct members +{ + constexpr static std::size_t value = Count; +}; + +template ::max()> +struct explicit_members +{ + constexpr static std::size_t value = Count; +}; + namespace concepts { namespace detail @@ -141,6 +154,23 @@ concept unspecialized = !container && !owning_pointer && !tuple && !variant && !optional && !std::is_array_v>; + +template +concept byte_serializable = requires +{ + requires std::is_trivially_copyable_v>; + requires std::integral_constant< + int, + (std::bit_cast>( + std::array()), + 0)>::value + == 0; +} +&&(!requires { + requires std::same_as< + explicit_members::serialize::value>, + typename std::remove_cvref_t::serialize>; +}); } // namespace concepts template @@ -333,12 +363,6 @@ constexpr auto failure(errc code) return std::errc{} != code; } -template -struct members -{ - constexpr static std::size_t value = Count; -}; - struct access { template @@ -404,15 +428,10 @@ struct access { return visit_members( item, [&](auto &&... items) constexpr { - if constexpr (std::is_trivially_copyable_v< - std::remove_cvref_t>) { + if constexpr (concepts::byte_serializable) { if constexpr (sizeof...(items) > 1 && - (... && - (std::is_fundamental_v< - std::remove_cvref_t< - decltype(items)>> || - std::is_enum_v>))) { + (... && concepts::byte_serializable< + decltype(items)>)) { return serializer.serialize_one(as_bytes(item)); } else { return serializer.serialize_many(items...); @@ -423,23 +442,34 @@ struct access }); } - constexpr static auto serialize_members( - auto &serializer, auto &item) requires std::is_same_v < - members::serialize::value>, - typename std::remove_cvref_t::serialize > - &&(std::remove_cvref_t::serialize::value <= - max_members_serialize) + constexpr static auto + serialize_members(auto & serializer, auto & item) requires( + std::is_same_v< + members< + std::remove_cvref_t::serialize::value>, + typename std::remove_cvref_t::serialize> || + std::is_same_v::serialize::value>, + typename std::remove_cvref_t< + decltype(item)>::serialize>) && + (std::remove_cvref_t::serialize::value <= + max_members_serialize) { return serialize_members< std::remove_cvref_t::serialize::value>( serializer, item); } - constexpr static auto serialize_members(auto & serializer, - auto & item) requires - std::is_same_v< - members::value>, - std::remove_cvref_t> && + constexpr static auto + serialize_members(auto & serializer, auto & item) requires( + std::is_same_v< + members< + std::remove_cvref_t::value>, + std::remove_cvref_t> || + std::is_same_v< + explicit_members< + std::remove_cvref_t::value>, + std::remove_cvref_t>) && (std::remove_cvref_t::value <= max_members_serialize) { @@ -450,11 +480,26 @@ struct access constexpr static auto serialize_members(auto & serializer, auto & item) requires( - (number_of_members>() >= 0) && - !requires { typename std::remove_cvref_t::serialize; } && - !requires { std::remove_cvref_t::serialize(serializer, item); } && - !requires { serialize(item); } && !requires { serialize(serializer, item); }) - + (number_of_members>() >= 0) && + ( + !requires { + typename std::remove_cvref_t::serialize; + } || + requires { + requires std::same_as < + explicit_members::serialize::value>, + typename std::remove_cvref_t::serialize > ; + requires std::remove_cvref_t< + decltype(item)>::serialize::value == + std::numeric_limits::max(); + }) && + !requires { + std::remove_cvref_t::serialize(serializer, + item); + } && + !requires { serialize(item); } && + !requires { serialize(serializer, item); }) { return serialize_members< number_of_members>()>( @@ -475,6 +520,9 @@ class basic_out template friend struct option; + template + using template_type = basic_out; + friend access; template @@ -564,7 +612,11 @@ class basic_out using type = std::remove_cvref_t; static_assert(!std::is_pointer_v); - if constexpr (std::is_fundamental_v || std::is_enum_v) { + if constexpr (requires { type::serialize(*this, item); }) { + return type::serialize(*this, item); + } else if constexpr (requires { serialize(*this, item); }) { + return serialize(*this, item); + } else if constexpr (std::is_fundamental_v || std::is_enum_v) { auto size = m_data.size(); if (m_position + sizeof(item) > size) [[unlikely]] { if constexpr (is_resizable) { @@ -622,10 +674,8 @@ class basic_out } m_position += item_size_in_bytes; return {}; - } else if constexpr (requires { type::serialize(*this, item); }) { - return type::serialize(*this, item); - } else if constexpr (requires { serialize(*this, item); }) { - return serialize(*this, item); + } else if constexpr (concepts::byte_serializable) { + return serialize_one(as_bytes(item)); } else { return access::serialize_members(*this, item); } @@ -636,8 +686,7 @@ class basic_out { using value_type = std::remove_cvref_t; - if constexpr (std::is_fundamental_v || - std::is_enum_v) { + if constexpr (concepts::byte_serializable) { return serialize_one(bytes(array)); } else { for (auto & item : array) { @@ -673,12 +722,11 @@ class basic_out } } - if constexpr ((std::is_fundamental_v || - std::is_enum_v)&&std:: - is_base_of_v:: - iterator_category> && + if constexpr (concepts::byte_serializable && + std::is_base_of_v:: + iterator_category> && requires { container.data(); }) { return serialize_one(bytes(container)); } else { @@ -760,6 +808,9 @@ template class out : public basic_out { public: + template + using template_type = out; + using basic_out::basic_out; constexpr auto operator()(auto &&... items) @@ -793,6 +844,9 @@ template class in { public: + template + using template_type = in; + template friend struct option; @@ -868,7 +922,11 @@ class in using type = std::remove_cvref_t; static_assert(!std::is_pointer_v); - if constexpr (std::is_fundamental_v || std::is_enum_v) { + if constexpr (requires { type::serialize(*this, item); }) { + return type::serialize(*this, item); + } else if constexpr (requires { serialize(*this, item); }) { + return serialize(*this, item); + } else if constexpr (std::is_fundamental_v || std::is_enum_v) { auto size = m_data.size(); if (m_position + sizeof(item) > size) [[unlikely]] { return std::errc::result_out_of_range; @@ -921,10 +979,8 @@ class in } m_position += item_size_in_bytes; return {}; - } else if constexpr (requires { type::serialize(*this, item); }) { - return type::serialize(*this, item); - } else if constexpr (requires { serialize(*this, item); }) { - return serialize(*this, item); + } else if constexpr (concepts::byte_serializable) { + return serialize_one(as_bytes(item)); } else { return access::serialize_members(*this, item); } @@ -935,8 +991,7 @@ class in { using value_type = std::remove_cvref_t; - if constexpr (std::is_fundamental_v || - std::is_enum_v) { + if constexpr (concepts::byte_serializable) { return serialize_one(bytes(array)); } else { for (auto & item : array) { @@ -981,12 +1036,11 @@ class in } } - if constexpr ((std::is_fundamental_v || - std::is_enum_v)&&std:: - is_base_of_v:: - iterator_category> && + if constexpr (concepts::byte_serializable && + std::is_base_of_v:: + iterator_category> && requires { container.data(); }) { return serialize_one(bytes(container)); } else {