From 727edc1c5456b865a6fe92cd955d44cf52726ff7 Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Sat, 24 Oct 2026 21:31:44 +0300 Subject: [PATCH] Omit fixed-array lengths in schema-driven archive formats --- include/rfl/boost_serialization/Reader.hpp | 24 ++-- include/rfl/boost_serialization/Writer.hpp | 30 +++++ include/rfl/cereal/Reader.hpp | 28 +++-- include/rfl/cereal/Writer.hpp | 30 +++++ include/rfl/parsing/Parent.hpp | 28 +++++ include/rfl/parsing/ParserArray.hpp | 52 +++++---- include/rfl/parsing/ParserCArray.hpp | 2 +- include/rfl/parsing/read_array_elements.hpp | 33 ++++++ include/rfl/yas/Reader.hpp | 29 ++--- include/rfl/yas/Writer.hpp | 30 +++++ .../test_fixed_array_io.cpp | 65 +++++++++++ tests/cereal/test_fixed_array_io.cpp | 61 ++++++++++ tests/check_fixed_array_io.hpp | 109 ++++++++++++++++++ tests/yas/test_fixed_array_io.cpp | 31 +++++ 14 files changed, 491 insertions(+), 61 deletions(-) create mode 100644 include/rfl/parsing/read_array_elements.hpp create mode 100644 tests/boost_serialization/test_fixed_array_io.cpp create mode 100644 tests/cereal/test_fixed_array_io.cpp create mode 100644 tests/check_fixed_array_io.hpp create mode 100644 tests/yas/test_fixed_array_io.cpp diff --git a/include/rfl/boost_serialization/Reader.hpp b/include/rfl/boost_serialization/Reader.hpp index 13626f69f..2008853b2 100644 --- a/include/rfl/boost_serialization/Reader.hpp +++ b/include/rfl/boost_serialization/Reader.hpp @@ -11,6 +11,7 @@ #include "../Result.hpp" #include "../always_false.hpp" #include "../internal/is_literal.hpp" +#include "../parsing/read_array_elements.hpp" namespace rfl::boost_serialization { @@ -140,19 +141,16 @@ struct Reader { template std::optional read_array(const ArrayReader& _array_reader, const InputArrayType& _arr) const noexcept { - if constexpr (requires { _array_reader.reserve(_arr.size); }) { - if (const auto err = _array_reader.reserve(_arr.size)) { - return err; - } - } - for (size_t i = 0; i < _arr.size; ++i) { - auto var = InputVarType{_arr.ar}; - const auto err = _array_reader.read(var); - if (err) { - return err; - } - } - return std::nullopt; + return parsing::read_array_elements(_array_reader, InputVarType{_arr.ar}, + _arr.size); + } + + /// Reads exactly the compile-time extent without consuming a length prefix. + template + std::optional read_fixed_array( + const ArrayReader& _array_reader, + const InputVarType& _var) const noexcept { + return parsing::read_array_elements(_array_reader, _var, _size); } /// Reads all key-value pairs from a map using the provided map reader. diff --git a/include/rfl/boost_serialization/Writer.hpp b/include/rfl/boost_serialization/Writer.hpp index 8c18842d5..9f497ae7e 100644 --- a/include/rfl/boost_serialization/Writer.hpp +++ b/include/rfl/boost_serialization/Writer.hpp @@ -46,6 +46,36 @@ class Writer { return OutputArrayType{}; } + /// Fixed-size arrays store only their elements; parent metadata is retained. + OutputArrayType fixed_array_as_root(const size_t /*_size*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_array(const size_t /*_size*/, + OutputArrayType* /*_parent*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_object( + const std::string_view& /*_name*/, const size_t /*_size*/, + OutputObjectType* /*_parent*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_map(const std::string_view& _name, + const size_t /*_size*/, + OutputMapType* /*_parent*/) const { + write_key(_name); + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_union(const size_t _index, + const size_t /*_size*/, + OutputUnionType* /*_parent*/) const { + write_index(_index); + return OutputArrayType{}; + } + /// Creates a map as the root element. /// Writes the size to the archive first. /// @param _size The number of entries in the map diff --git a/include/rfl/cereal/Reader.hpp b/include/rfl/cereal/Reader.hpp index 930be1410..7f06be0ab 100644 --- a/include/rfl/cereal/Reader.hpp +++ b/include/rfl/cereal/Reader.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include "../always_false.hpp" #include "../internal/always_inline.hpp" #include "../internal/is_literal.hpp" +#include "../parsing/read_array_elements.hpp" #include "../parsing/schemaful/IsSchemafulReader.hpp" namespace rfl::cereal { @@ -129,18 +131,20 @@ struct Reader { try { size_t size; (*_arr.archive_)(::cereal::make_size_tag(size)); - if constexpr (requires { _array_reader.reserve(size); }) { - if (const auto err = _array_reader.reserve(size)) { - return err; - } - } - for (size_t i = 0; i < size; ++i) { - const auto err = _array_reader.read(InputVarType{_arr.archive_}); - if (err) { - return err; - } - } - return std::nullopt; + return parsing::read_array_elements(_array_reader, + InputVarType{_arr.archive_}, size); + } catch (std::exception& e) { + return Error(std::string("Cereal array read error: ") + e.what()); + } + } + + /// Reads exactly the compile-time extent without consuming a length prefix. + template + std::optional read_fixed_array( + const ArrayReader& _array_reader, + const InputVarType& _var) const noexcept { + try { + return parsing::read_array_elements(_array_reader, _var, _size); } catch (std::exception& e) { return Error(std::string("Cereal array read error: ") + e.what()); } diff --git a/include/rfl/cereal/Writer.hpp b/include/rfl/cereal/Writer.hpp index 15a2993fe..1e857c46d 100644 --- a/include/rfl/cereal/Writer.hpp +++ b/include/rfl/cereal/Writer.hpp @@ -54,6 +54,36 @@ class Writer { /// @return An OutputArrayType OutputArrayType array_as_root(const size_t _size) const; + /// Fixed-size arrays store only their elements; parent metadata is retained. + OutputArrayType fixed_array_as_root(const size_t /*_size*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_array(const size_t /*_size*/, + OutputArrayType* /*_parent*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_object( + const std::string_view& /*_name*/, const size_t /*_size*/, + OutputObjectType* /*_parent*/) const { + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_map(const std::string_view& _name, + const size_t /*_size*/, + OutputMapType* /*_parent*/) const { + add_string_view(_name); + return OutputArrayType{}; + } + + OutputArrayType add_fixed_array_to_union(const size_t _index, + const size_t /*_size*/, + OutputUnionType* /*_parent*/) const { + (*archive_)(_index); + return OutputArrayType{}; + } + /// Creates a map as the root element. /// Writes the size tag to the archive first. /// @param _size The number of entries in the map diff --git a/include/rfl/parsing/Parent.hpp b/include/rfl/parsing/Parent.hpp index 5d868d4cb..9c3e2756c 100644 --- a/include/rfl/parsing/Parent.hpp +++ b/include/rfl/parsing/Parent.hpp @@ -81,6 +81,34 @@ struct Parent { } } + /// Adds a fixed-size array, omitting its length when the writer supports it. + template + static OutputArrayType add_fixed_array(const W& _w, const size_t _size, + const ParentType& _parent) { + using Type = std::remove_cvref_t; + if constexpr (!requires { _w.fixed_array_as_root(_size); }) { + return add_array(_w, _size, _parent); + } else if constexpr (std::is_same_v) { + return _w.add_fixed_array_to_array(_size, _parent.arr_); + } else if constexpr (std::is_same_v) { + return _w.add_fixed_array_to_object(_parent.name_, _size, _parent.obj_); + } else if constexpr (std::is_same_v) { + return _w.fixed_array_as_root(_size); + } else if constexpr (schemaful::IsSchemafulWriter) { + if constexpr (std::is_same_v>) { + return _w.add_fixed_array_to_map(_parent.name_, _size, _parent.map_); + } else if constexpr (std::is_same_v>) { + return _w.add_fixed_array_to_union(_parent.index_, _size, + _parent.union_); + } else { + static_assert(always_false_v, "Unsupported option."); + } + } else { + static_assert(always_false_v, "Unsupported option."); + } + } + /** * @brief Adds a comment to the parent. * diff --git a/include/rfl/parsing/ParserArray.hpp b/include/rfl/parsing/ParserArray.hpp index 66cc17228..8a9a66de4 100644 --- a/include/rfl/parsing/ParserArray.hpp +++ b/include/rfl/parsing/ParserArray.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "../Ref.hpp" @@ -45,29 +46,36 @@ struct ParserArray { */ static Result> read(const R& _r, const InputVarType& _var) noexcept { - const auto parse = - [&](const InputArrayType& _arr) -> Result> { - alignas( - std::array) unsigned char buf[sizeof(std::array)]; - auto ptr = internal::ptr_cast*>(&buf); - const auto array_reader = - ArrayReader(&_r, ptr); - auto err = _r.read_array(array_reader, _arr); - if (err) { - call_destructors_on_array_where_necessary(array_reader.num_set(), ptr); - return error(*err); + alignas( + std::array) unsigned char buf[sizeof(std::array)]; + auto ptr = internal::ptr_cast*>(&buf); + const auto array_reader = + ArrayReader(&_r, ptr); + auto err = [&]() -> std::optional { + if constexpr (requires { + _r.template read_fixed_array<_size>(array_reader, _var); + }) { + return _r.template read_fixed_array<_size>(array_reader, _var); + } else { + const auto arr = _r.to_array(_var); + if (!arr) { + return arr.error(); + } + return _r.read_array(array_reader, *arr); } - err = array_reader.check_size(); - if (err) { - call_destructors_on_array_where_necessary(array_reader.num_set(), ptr); - return error(*err); - } - auto result = Result>(std::move(*ptr)); + }(); + if (err) { call_destructors_on_array_where_necessary(array_reader.num_set(), ptr); - return result; - }; - - return _r.to_array(_var).and_then(parse); + return error(*err); + } + err = array_reader.check_size(); + if (err) { + call_destructors_on_array_where_necessary(array_reader.num_set(), ptr); + return error(*err); + } + auto result = Result>(std::move(*ptr)); + call_destructors_on_array_where_necessary(array_reader.num_set(), ptr); + return result; } /** @@ -80,7 +88,7 @@ struct ParserArray { template static void write(const W& _w, const std::array& _arr, const P& _parent) { - auto arr = ParentType::add_array(_w, _size, _parent); + auto arr = ParentType::add_fixed_array(_w, _size, _parent); const auto new_parent = typename ParentType::Array{&arr}; for (const auto& e : _arr) { Parser, ProcessorsType>::write(_w, e, diff --git a/include/rfl/parsing/ParserCArray.hpp b/include/rfl/parsing/ParserCArray.hpp index 8c17aec08..c21b5286e 100644 --- a/include/rfl/parsing/ParserCArray.hpp +++ b/include/rfl/parsing/ParserCArray.hpp @@ -58,7 +58,7 @@ struct ParserCArray { */ template static void write(const W& _w, const CArray& _arr, const P& _parent) { - auto arr = ParentType::add_array(_w, _size, _parent); + auto arr = ParentType::add_fixed_array(_w, _size, _parent); const auto new_parent = typename ParentType::Array{&arr}; for (const auto& e : _arr) { Parser, ProcessorsType>::write(_w, e, diff --git a/include/rfl/parsing/read_array_elements.hpp b/include/rfl/parsing/read_array_elements.hpp new file mode 100644 index 000000000..b8025e962 --- /dev/null +++ b/include/rfl/parsing/read_array_elements.hpp @@ -0,0 +1,33 @@ +#ifndef RFL_PARSING_READ_ARRAY_ELEMENTS_HPP_ +#define RFL_PARSING_READ_ARRAY_ELEMENTS_HPP_ + +#include +#include + +#include "../Result.hpp" + +namespace rfl::parsing { + +/// Reads elements from a sequential archive after its extent is known. +/// Fixed-size readers supply their compile-time extent; dynamic readers first +/// consume the length prefix. Each element advances the archive in _var. +template +std::optional read_array_elements(const ArrayReader& _array_reader, + const InputVarType& _var, + const std::size_t _size) { + if constexpr (requires { _array_reader.reserve(_size); }) { + if (const auto err = _array_reader.reserve(_size)) { + return err; + } + } + for (std::size_t i = 0; i < _size; ++i) { + if (const auto err = _array_reader.read(_var)) { + return err; + } + } + return std::nullopt; +} + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/yas/Reader.hpp b/include/rfl/yas/Reader.hpp index c06dffc3e..86ac8b450 100644 --- a/include/rfl/yas/Reader.hpp +++ b/include/rfl/yas/Reader.hpp @@ -14,6 +14,7 @@ #include "../Result.hpp" #include "../always_false.hpp" #include "../internal/is_literal.hpp" +#include "../parsing/read_array_elements.hpp" #include "../parsing/schemaful/IsSchemafulReader.hpp" namespace rfl::yas { @@ -119,20 +120,22 @@ struct Reader { std::optional read_array(const ArrayReader& _array_reader, const InputArrayType& _arr) const noexcept { try { - std::size_t size; + size_t size; (*_arr.ar) & size; - if constexpr (requires { _array_reader.reserve(size); }) { - if (const auto err = _array_reader.reserve(size)) { - return err; - } - } - for (std::size_t i = 0; i < size; ++i) { - const auto err = _array_reader.read(InputVarType{_arr.ar}); - if (err) { - return err; - } - } - return std::nullopt; + return parsing::read_array_elements(_array_reader, InputVarType{_arr.ar}, + size); + } catch (std::exception& e) { + return Error(std::string("yas array read error: ") + e.what()); + } + } + + /// Reads exactly the compile-time extent without consuming a length prefix. + template + std::optional read_fixed_array( + const ArrayReader& _array_reader, + const InputVarType& _var) const noexcept { + try { + return parsing::read_array_elements(_array_reader, _var, _size); } catch (std::exception& e) { return Error(std::string("yas array read error: ") + e.what()); } diff --git a/include/rfl/yas/Writer.hpp b/include/rfl/yas/Writer.hpp index 4d2352587..2d3671bc6 100644 --- a/include/rfl/yas/Writer.hpp +++ b/include/rfl/yas/Writer.hpp @@ -57,6 +57,36 @@ struct Writer { return OutputArrayType{ar_}; } + /// Fixed-size arrays store only their elements; parent metadata is retained. + OutputArrayType fixed_array_as_root(const size_t /*_size*/) const { + return OutputArrayType{ar_}; + } + + OutputArrayType add_fixed_array_to_array(const size_t /*_size*/, + OutputArrayType* /*_parent*/) const { + return OutputArrayType{ar_}; + } + + OutputArrayType add_fixed_array_to_object( + const std::string_view& /*_name*/, const size_t /*_size*/, + OutputObjectType* /*_parent*/) const { + return OutputArrayType{ar_}; + } + + OutputArrayType add_fixed_array_to_map(const std::string_view& _name, + const size_t /*_size*/, + OutputMapType* /*_parent*/) const { + add_string_view(_name); + return OutputArrayType{ar_}; + } + + OutputArrayType add_fixed_array_to_union(const size_t _index, + const size_t /*_size*/, + OutputUnionType* /*_parent*/) const { + (*ar_) & _index; + return OutputArrayType{ar_}; + } + /// /// @brief Begins serializing a map as the root object. /// @param _size The number of key-value pairs in the map. diff --git a/tests/boost_serialization/test_fixed_array_io.cpp b/tests/boost_serialization/test_fixed_array_io.cpp new file mode 100644 index 000000000..584cbe73c --- /dev/null +++ b/tests/boost_serialization/test_fixed_array_io.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +#include "../check_fixed_array_io.hpp" + +namespace test_fixed_array_io { + +TEST(boost_serialization, fixed_array_size_and_roundtrip) { + check_sizes( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }, + sizeof(std::uint64_t)); +} + +TEST(boost_serialization, fixed_array_parent_metadata) { + check_parents( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); + check_parents( + [](const auto& input) { + return rfl::boost_serialization::write(input); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); +} + +} // namespace test_fixed_array_io + +namespace test_fixed_array_io { + +TEST(boost_serialization, fixed_array_text_archive) { + using IArchive = boost::archive::text_iarchive; + using OArchive = boost::archive::text_oarchive; + const std::array input{"hello", "world"}; + std::ostringstream actual; + { + OArchive archive(actual); + rfl::boost_serialization::write(archive, input); + } + std::ostringstream expected; + { + OArchive archive(expected); + for (const auto& value : input) { + archive << value; + } + } + EXPECT_EQ(actual.str(), expected.str()); + std::istringstream stream(expected.str()); + IArchive archive(stream); + const auto decoded = + rfl::boost_serialization::read_from_archive, + IArchive, OArchive>(archive); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +} // namespace test_fixed_array_io diff --git a/tests/cereal/test_fixed_array_io.cpp b/tests/cereal/test_fixed_array_io.cpp new file mode 100644 index 000000000..a6c9862c7 --- /dev/null +++ b/tests/cereal/test_fixed_array_io.cpp @@ -0,0 +1,61 @@ +#include +#include +#include + +#include "../check_fixed_array_io.hpp" + +namespace test_fixed_array_io { + +static_assert(sizeof(rfl::cereal::Reader::InputArrayType) == + sizeof(rfl::cereal::Reader::CerealArchive*)); + +TEST(cereal, fixed_array_size_and_roundtrip) { + check_sizes( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + sizeof(::cereal::size_type)); +} + +TEST(cereal, fixed_array_parent_metadata) { + check_parents( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }); + check_parents( + [](const auto& input) { + return rfl::cereal::write(input); + }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} + +} // namespace test_fixed_array_io + +namespace test_fixed_array_io { + +TEST(cereal, fixed_array_endianness_and_payload) { + using Archive = ::cereal::PortableBinaryOutputArchive; + const std::array input{12, -34, 56}; + for (const auto options : + {Archive::Options::LittleEndian(), Archive::Options::BigEndian()}) { + std::ostringstream actual; + { + Archive archive(actual, options); + rfl::cereal::write(input, archive); + } + std::ostringstream expected; + { + Archive archive(expected, options); + for (const auto value : input) { + archive(value); + } + } + EXPECT_EQ(actual.str(), expected.str()); + const auto decoded = + rfl::cereal::read>(expected.str()); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + } +} + +} // namespace test_fixed_array_io diff --git a/tests/check_fixed_array_io.hpp b/tests/check_fixed_array_io.hpp new file mode 100644 index 000000000..535f3e046 --- /dev/null +++ b/tests/check_fixed_array_io.hpp @@ -0,0 +1,109 @@ +#ifndef CHECK_FIXED_ARRAY_IO_HPP_ +#define CHECK_FIXED_ARRAY_IO_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_fixed_array_io { + +template +void check_roundtrip(const T& input, Write write, Read read) { + const auto decoded = read.template operator()(write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +template +void check_sizes(Write write, Read read, const size_t prefix_size) { + using Array = std::array; + const Array input{12, -34, 56}; + const auto bytes = write(input); + const auto header_size = write(std::int32_t{0}).size() - sizeof(std::int32_t); + EXPECT_EQ(bytes.size(), header_size + 3 * sizeof(std::int32_t)); + EXPECT_EQ(write(std::vector(input.begin(), input.end())).size(), + bytes.size() + prefix_size); + check_roundtrip(input, write, read); + + const std::array empty{}; + EXPECT_EQ(write(empty).size(), header_size); + check_roundtrip(empty, write, read); + check_roundtrip(std::array{true, false, true}, write, read); + check_roundtrip(std::array{"hello", "world"}, write, read); + + const std::array, 2> nested{{{1, 2}, {3, 4}}}; + EXPECT_EQ(write(nested).size(), header_size + 4 * sizeof(std::int32_t)); + check_roundtrip(nested, write, read); + check_roundtrip(std::array, 3>{}, write, read); + check_roundtrip(std::array, 2>{{{1, 2}, {}}}, write, read); + + auto truncated = bytes; + truncated.pop_back(); + EXPECT_FALSE(read.template operator()(truncated)); + auto strings = write(std::array{"first", "second"}); + strings.pop_back(); + const auto invalid = + read.template operator()>(strings); + ASSERT_FALSE(invalid); + EXPECT_NE(invalid.error().what().find("element 1"), std::string::npos); +} + +struct Nested { + std::array values; + std::array empty; + std::vector> rows; + std::map> lookup; + std::optional> optional; + std::variant> choice; + int tail; + + bool operator==(const Nested&) const = default; +}; + +template +void check_parents(Write write, Read read) { + const Nested input{{1, 2}, + {}, + {{3, 4}, {5, 6}}, + {{"key", {7, 8}}}, + std::array{9, 10}, + std::array{11, 12}, + 42}; + check_roundtrip(input, write, read); + check_roundtrip(std::variant>{std::array{}}, + write, read); + check_roundtrip( + std::map>{{"a", {}}, {"b", {}}}, write, + read); + + // C arrays share the std::array reader, so their writer must agree with it. + const int c_arrays[2][2]{{1, 2}, {3, 4}}; + using CArray = int[2][2]; + const auto decoded = read.template operator()(write(c_arrays)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(decoded->arr_[0][0], 1); + EXPECT_EQ(decoded->arr_[0][1], 2); + EXPECT_EQ(decoded->arr_[1][0], 3); + EXPECT_EQ(decoded->arr_[1][1], 4); + + using MoveOnly = std::array, 2>; + const MoveOnly pointers{std::make_unique(13), std::make_unique(14)}; + const auto moved = read.template operator()(write(pointers)); + ASSERT_TRUE(moved) << moved.error().what(); + ASSERT_TRUE((*moved)[0]); + ASSERT_TRUE((*moved)[1]); + EXPECT_EQ(*(*moved)[0], 13); + EXPECT_EQ(*(*moved)[1], 14); +} + +} // namespace test_fixed_array_io + +#endif diff --git a/tests/yas/test_fixed_array_io.cpp b/tests/yas/test_fixed_array_io.cpp new file mode 100644 index 000000000..492866f60 --- /dev/null +++ b/tests/yas/test_fixed_array_io.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "../check_fixed_array_io.hpp" + +namespace test_fixed_array_io { + +static_assert(sizeof(rfl::yas::Reader::InputArrayType) == + sizeof(rfl::yas::Reader::IArchive*)); + +TEST(yas, fixed_array_size_and_roundtrip) { + check_sizes( + [](const auto& input) { return rfl::yas::write(input); }, + [](const auto& bytes) { return rfl::yas::read(bytes); }, + sizeof(size_t)); +} + +TEST(yas, fixed_array_parent_metadata) { + check_parents( + [](const auto& input) { return rfl::yas::write(input); }, + [](const auto& bytes) { return rfl::yas::read(bytes); }); + check_parents( + [](const auto& input) { + return rfl::yas::write(input); + }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +} + +} // namespace test_fixed_array_io