From b2327ac444c63224a045dc847ca7274bfee51645 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:40:37 +0300 Subject: [PATCH 1/6] Dispatch Cereal object fields with compile-time indices Select the known field directly through the existing ViewReader assignment path instead of scanning every field for each runtime index. Preserve validation, error collection, and cleanup behavior. Add regression coverage for multiple field errors, truncated input cleanup, empty objects, and runtime-index reader compatibility. --- .github/workflows/linux.yaml | 1 + .github/workflows/macos.yaml | 1 + .github/workflows/windows.yaml | 1 + benchmarks/all/field_dispatch.hpp | 76 +++++++++++++ benchmarks/all/field_dispatch_cereal.cpp | 19 ++++ include/rfl/cereal/Reader.hpp | 8 +- include/rfl/parsing/ViewReader.hpp | 7 ++ tests/cereal/test_field_dispatch.cpp | 40 +++++++ tests/check_positional_field_dispatch.hpp | 123 ++++++++++++++++++++++ 9 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 benchmarks/all/field_dispatch.hpp create mode 100644 benchmarks/all/field_dispatch_cereal.cpp create mode 100644 tests/cereal/test_field_dispatch.cpp create mode 100644 tests/check_positional_field_dispatch.hpp diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b11ed0ec..8e8c26af7 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -121,6 +121,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 40ab577d1..b90bee2fc 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -63,6 +63,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 14ca7b2fc..2178fdce3 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -62,6 +62,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/field_dispatch.hpp b/benchmarks/all/field_dispatch.hpp new file mode 100644 index 000000000..c78090d25 --- /dev/null +++ b/benchmarks/all/field_dispatch.hpp @@ -0,0 +1,76 @@ +#ifndef BENCHMARK_FIELD_DISPATCH_HPP_ +#define BENCHMARK_FIELD_DISPATCH_HPP_ +#include + +#include +#include +namespace field_dispatch { +struct Record2 { + float field0 = 0; + float field1 = 1; + bool operator==(const Record2&) const = default; +}; +struct Record16 { + float field0 = 0; + float field1 = 1; + float field2 = 2; + float field3 = 3; + float field4 = 4; + float field5 = 5; + float field6 = 6; + float field7 = 7; + float field8 = 8; + float field9 = 9; + float field10 = 10; + float field11 = 11; + float field12 = 12; + float field13 = 13; + float field14 = 14; + float field15 = 15; + bool operator==(const Record16&) const = default; +}; +template +struct Data { + std::vector records; + bool operator==(const Data&) const = default; +}; +template +auto make_data(std::size_t size) { + Data data; + data.records.resize(size); + for (std::size_t i = 0; i < size; ++i) { + data.records[i].field0 = static_cast(i) * 0.5f; + data.records[i].field1 = -static_cast(i) * 0.25f; + } + return data; +} +template +void run(benchmark::State& state, Write write, Read read) { + const auto input = make_data(state.range(0)); + const auto bytes = write(input); + { + const auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != input) { + state.SkipWithError("Fields changed during round trip"); + return; + } + } + for (auto _ : state) { + auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * state.range(0)); + state.SetBytesProcessed(state.iterations() * + static_cast(bytes.size())); + state.counters["serialized_bytes"] = static_cast(bytes.size()); +} +} // namespace field_dispatch +#endif diff --git a/benchmarks/all/field_dispatch_cereal.cpp b/benchmarks/all/field_dispatch_cereal.cpp new file mode 100644 index 000000000..a1dc6085a --- /dev/null +++ b/benchmarks/all/field_dispatch_cereal.cpp @@ -0,0 +1,19 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_cereal(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::cereal::write(input); }, + [&](const auto& bytes) { + return rfl::cereal::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_cereal, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_cereal, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/cereal/Reader.hpp b/include/rfl/cereal/Reader.hpp index 85d1ab09d..3fabeb2e1 100644 --- a/include/rfl/cereal/Reader.hpp +++ b/include/rfl/cereal/Reader.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../Bytestring.hpp" #include "../Result.hpp" @@ -171,9 +172,10 @@ struct Reader { std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { try { - for (size_t i = 0; i < ObjectReader::size(); ++i) { - _object_reader.read(i, InputVarType{_obj.archive_}); - } + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.archive_}), ...); + }(std::make_integer_sequence{}); return std::nullopt; } catch (std::exception& e) { return Error(std::string("Cereal object read error: ") + e.what()); diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 9082ac9e3..7c8d798ae 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -62,6 +62,13 @@ class ViewReader { std::make_integer_sequence()); } + // Positional readers already know the field at compile time. + template + void read(std::integral_constant, const InputVarType& _var) const { + assign_to_matching_field(*r_, i, _var, view_, errors_, found_, set_, + std::integer_sequence()); + } + /** * @brief Returns the size of the view. * diff --git a/tests/cereal/test_field_dispatch.cpp b/tests/cereal/test_field_dispatch.cpp new file mode 100644 index 000000000..d9bcc66cf --- /dev/null +++ b/tests/cereal/test_field_dispatch.cpp @@ -0,0 +1,40 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_cereal = [](const auto& input) { + return rfl::cereal::write(input); +}; +const auto read_cereal = [](const auto& bytes) { + return rfl::cereal::read(bytes); +}; +TEST(cereal, field_dispatch_errors_and_cleanup) { + check_errors(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_empty_object) { + check_empty(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_truncated_cleanup) { + check_truncated(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_static_indices) { + int next = 0; + int static_calls = 0; + const rfl::cereal::Reader reader; + EXPECT_FALSE(reader.read_object( + IndexedReader{{&next}, &static_calls}, + {nullptr})); + EXPECT_EQ(next, 3); + EXPECT_EQ(static_calls, 3); +} +struct CerealRuntimeIndexReader : RuntimeIndexReader { + static constexpr int size() { return 3; } +}; +TEST(cereal, field_dispatch_runtime_index_reader_compatibility) { + int next = 0; + const rfl::cereal::Reader reader; + EXPECT_FALSE( + reader.read_object(CerealRuntimeIndexReader{{&next}}, {nullptr})); + EXPECT_EQ(next, 3); +} +} // namespace test_positional_field_dispatch diff --git a/tests/check_positional_field_dispatch.hpp b/tests/check_positional_field_dispatch.hpp new file mode 100644 index 000000000..3a3a0b0ef --- /dev/null +++ b/tests/check_positional_field_dispatch.hpp @@ -0,0 +1,123 @@ +#ifndef CHECK_POSITIONAL_FIELD_DISPATCH_HPP_ +#define CHECK_POSITIONAL_FIELD_DISPATCH_HPP_ +#include + +#include +#include +#include +namespace test_positional_field_dispatch { +struct Input { + int first; + std::string middle; + int last; +}; + +class TrackedString { + public: + using ReflectionType = std::string; + inline static int live = 0; + + explicit TrackedString(std::string value) : value_(std::move(value)) { + ++live; + } + TrackedString(TrackedString&& other) : value_(std::move(other.value_)) { + ++live; + } + ~TrackedString() { --live; } + const std::string& reflection() const { return value_; } + + private: + std::string value_; +}; + +struct Validated { + rfl::Validator> first; + TrackedString middle; + rfl::Validator> last; +}; + +template +void check_errors(Write write, Read read) { + const auto bytes = write(Input{-1, "constructed between errors", -2}); + const auto result = read.template operator()(bytes); + ASSERT_FALSE(result); + const std::string message = result.error().what(); + EXPECT_NE(message.find("Found 2 errors:"), std::string::npos); + const auto first = message.find("Failed to parse field 'first'"); + const auto last = message.find("Failed to parse field 'last'"); + ASSERT_NE(first, std::string::npos); + ASSERT_NE(last, std::string::npos); + EXPECT_LT(first, last); + EXPECT_EQ(TrackedString::live, 0); +} + +template +void check_truncated(Write write, Read read) { + auto bytes = write(Input{1, "constructed before truncated field", 2}); + bytes.pop_back(); + const auto result = read.template operator()(bytes); + ASSERT_FALSE(result); + EXPECT_NE( + std::string(result.error().what()).find("Failed to parse field 'last'"), + std::string::npos); + EXPECT_EQ(TrackedString::live, 0); +} + +struct Empty {}; +struct WithEmpty { + Empty empty; + int value; +}; +template +void check_empty(Write write, Read read) { + const auto result = + read.template operator()(write(WithEmpty{{}, 42})); + ASSERT_TRUE(result); + EXPECT_EQ(result->value, 42); +} + +template +struct RuntimeIndexReader { + int* next; + void read(int index, const typename R::InputVarType&) const { + EXPECT_EQ(index, (*next)++); + } +}; + +template +struct IndexedReader : RuntimeIndexReader { + int* static_calls; + static constexpr int size() { return N; } + using RuntimeIndexReader::read; + template + void read(std::integral_constant, + const typename R::InputVarType& var) const { + ++*static_calls; + RuntimeIndexReader::read(i, var); + } +}; + +template +void check_count(const R& reader, const typename R::InputObjectType& obj, + int count) { + int next = 0; + int static_calls = 0; + EXPECT_FALSE( + reader.read_object(IndexedReader{{&next}, &static_calls}, obj)); + EXPECT_EQ(next, count); + EXPECT_EQ(static_calls, N == count ? count : 0); +} + +template +void check_counts(const R& reader, const typename R::InputObjectType& obj, + int count) { + check_count<0>(reader, obj, count); + check_count<2>(reader, obj, count); + check_count<3>(reader, obj, count); + check_count<4>(reader, obj, count); + int next = 0; + EXPECT_FALSE(reader.read_object(RuntimeIndexReader{&next}, obj)); + EXPECT_EQ(next, count); +} +} // namespace test_positional_field_dispatch +#endif From 57f98348d1292b6aedb8fa6e4ce5e31d9df8e024 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:55:34 +0300 Subject: [PATCH 2/6] Dispatch YAS object fields with compile-time indices --- benchmarks/all/field_dispatch_yas.cpp | 17 +++++++++++++++++ include/rfl/yas/Reader.hpp | 9 ++++++--- tests/yas/test_field_dispatch.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 benchmarks/all/field_dispatch_yas.cpp create mode 100644 tests/yas/test_field_dispatch.cpp diff --git a/benchmarks/all/field_dispatch_yas.cpp b/benchmarks/all/field_dispatch_yas.cpp new file mode 100644 index 000000000..28357a8bf --- /dev/null +++ b/benchmarks/all/field_dispatch_yas.cpp @@ -0,0 +1,17 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_yas(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::yas::write(input); }, + [&](const auto& bytes) { return rfl::yas::read>(bytes); }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_yas, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_yas, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/yas/Reader.hpp b/include/rfl/yas/Reader.hpp index bf1b652cd..12d729c4f 100644 --- a/include/rfl/yas/Reader.hpp +++ b/include/rfl/yas/Reader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -163,9 +164,11 @@ struct Reader { std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { try { - for (std::size_t i = 0; i < ObjectReader::size(); ++i) { - _object_reader.read(i, InputVarType{_obj.ar}); - } + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.ar}), + ...); + }(std::make_integer_sequence{}); return std::nullopt; } catch (std::exception& e) { return Error(std::string("yas object read error: ") + e.what()); diff --git a/tests/yas/test_field_dispatch.cpp b/tests/yas/test_field_dispatch.cpp new file mode 100644 index 000000000..5924e662a --- /dev/null +++ b/tests/yas/test_field_dispatch.cpp @@ -0,0 +1,25 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_yas = [](const auto& input) { return rfl::yas::write(input); }; +const auto read_yas = [](const auto& bytes) { + return rfl::yas::read(bytes); +}; +TEST(yas, field_dispatch_errors_and_cleanup) { + check_errors(write_yas, read_yas); +} +TEST(yas, field_dispatch_empty_object) { check_empty(write_yas, read_yas); } +TEST(yas, field_dispatch_truncated_cleanup) { + check_truncated(write_yas, read_yas); +} +TEST(yas, field_dispatch_static_indices) { + int next = 0; + int static_calls = 0; + const rfl::yas::Reader reader; + EXPECT_FALSE(reader.read_object( + IndexedReader{{&next}, &static_calls}, {nullptr})); + EXPECT_EQ(next, 3); + EXPECT_EQ(static_calls, 3); +} +} // namespace test_positional_field_dispatch From 6dbe80b0e3545cf9ca9be3686da85418eb6dbd16 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:55:34 +0300 Subject: [PATCH 3/6] Dispatch matching Boost object fields with compile-time indices --- .../field_dispatch_boost_serialization.cpp | 20 ++++++++++++++ include/rfl/boost_serialization/Reader.hpp | 13 +++++++++ .../test_field_dispatch.cpp | 27 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 benchmarks/all/field_dispatch_boost_serialization.cpp create mode 100644 tests/boost_serialization/test_field_dispatch.cpp diff --git a/benchmarks/all/field_dispatch_boost_serialization.cpp b/benchmarks/all/field_dispatch_boost_serialization.cpp new file mode 100644 index 000000000..b99e034b2 --- /dev/null +++ b/benchmarks/all/field_dispatch_boost_serialization.cpp @@ -0,0 +1,20 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_boost_serialization(benchmark::State& state) { + run( + state, + [&](const auto& input) { return rfl::boost_serialization::write(input); }, + [&](const auto& bytes) { + return rfl::boost_serialization::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_boost_serialization, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_boost_serialization, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/boost_serialization/Reader.hpp b/include/rfl/boost_serialization/Reader.hpp index ec15eb0ae..ce71137d2 100644 --- a/include/rfl/boost_serialization/Reader.hpp +++ b/include/rfl/boost_serialization/Reader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../Result.hpp" #include "../always_false.hpp" @@ -181,6 +182,18 @@ struct Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (_obj.size == ObjectReader::size()) { + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.ar}), + ...); + }(std::make_integer_sequence{}); + return std::nullopt; + } + } for (size_t i = 0; i < _obj.size; ++i) { auto var = InputVarType{_obj.ar}; _object_reader.read(static_cast(i), var); diff --git a/tests/boost_serialization/test_field_dispatch.cpp b/tests/boost_serialization/test_field_dispatch.cpp new file mode 100644 index 000000000..a4da68e2d --- /dev/null +++ b/tests/boost_serialization/test_field_dispatch.cpp @@ -0,0 +1,27 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_boost_serialization = [](const auto& input) { + return rfl::boost_serialization::write(input); +}; +const auto read_boost_serialization = [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); +}; +TEST(boost_serialization, field_dispatch_errors_and_cleanup) { + check_errors(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_empty_object) { + check_empty(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_truncated_cleanup) { + check_truncated(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_input_counts) { + const rfl::boost_serialization::Reader + reader; + for (const auto count : {0, 2, 3, 4}) { + check_counts(reader, {nullptr, static_cast(count)}, count); + } +} +} // namespace test_positional_field_dispatch From 5fd3f9c8b11a6a8026aebee2aa32278c68913fba Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:55:34 +0300 Subject: [PATCH 4/6] Dispatch matching Avro record fields with compile-time indices --- benchmarks/all/field_dispatch_avro.cpp | 20 +++++++++++++++++++ include/rfl/avro/Reader.hpp | 27 ++++++++++++++++++++++++++ tests/avro/test_field_dispatch.cpp | 23 ++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 benchmarks/all/field_dispatch_avro.cpp create mode 100644 tests/avro/test_field_dispatch.cpp diff --git a/benchmarks/all/field_dispatch_avro.cpp b/benchmarks/all/field_dispatch_avro.cpp new file mode 100644 index 000000000..8dfc2a989 --- /dev/null +++ b/benchmarks/all/field_dispatch_avro.cpp @@ -0,0 +1,20 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_avro(benchmark::State& state) { + const auto schema = rfl::avro::to_schema>(); + run( + state, [&](const auto& input) { return rfl::avro::write(input, schema); }, + [&](const auto& bytes) { + return rfl::avro::read>(bytes, schema); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_avro, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_avro, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/avro/Reader.hpp b/include/rfl/avro/Reader.hpp index 747bb0d0c..75c478439 100644 --- a/include/rfl/avro/Reader.hpp +++ b/include/rfl/avro/Reader.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../Result.hpp" #include "../always_false.hpp" @@ -274,6 +275,32 @@ struct Reader { return Error(std::string(__FUNCTION__) + " error(" + std::to_string(res) + "): " + avro_strerror()); } + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (size == ObjectReader::size()) { + std::optional err; + const auto read_field = + [&](std::integral_constant index) { + if (err) { + return; + } + avro_value_t element; + const int result = + avro_value_get_by_index(_obj.val_, i, &element, nullptr); + if (result != 0) { + err = Error(std::string("read_object error(") + + std::to_string(result) + "): " + avro_strerror()); + return; + } + _object_reader.read(index, InputVarType{&element}); + }; + [&](std::integer_sequence) { + (read_field(std::integral_constant{}), ...); + }(std::make_integer_sequence{}); + return err; + } + } for (size_t ix = 0; ix < size; ++ix) { avro_value_t element; res = avro_value_get_by_index(_obj.val_, ix, &element, nullptr); diff --git a/tests/avro/test_field_dispatch.cpp b/tests/avro/test_field_dispatch.cpp new file mode 100644 index 000000000..e60874e17 --- /dev/null +++ b/tests/avro/test_field_dispatch.cpp @@ -0,0 +1,23 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_avro = [](const auto& input) { + return rfl::avro::write(input); +}; +const auto read_avro = [](const auto& bytes) { + return rfl::avro::read(bytes); +}; +TEST(avro, field_dispatch_errors_and_cleanup) { + check_errors(write_avro, read_avro); +} +TEST(avro, field_dispatch_empty_object) { check_empty(write_avro, read_avro); } +TEST(avro, field_dispatch_schema_counts) { + const auto schema = rfl::avro::to_schema(); + avro_value_t root; + ASSERT_EQ(avro_generic_value_new(schema.iface(), &root), 0); + const rfl::avro::Reader reader; + check_counts(reader, {&root}, 3); + avro_value_decref(&root); +} +} // namespace test_positional_field_dispatch From e0380c28747f0510b183bac8ca7bd232a1a94495 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:55:34 +0300 Subject: [PATCH 5/6] Dispatch matching CapnProto fields with compile-time indices --- benchmarks/all/field_dispatch_capnproto.cpp | 21 +++++++++++++++++ include/rfl/capnproto/Reader.hpp | 16 ++++++++++++- tests/capnproto/test_field_dispatch.cpp | 26 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 benchmarks/all/field_dispatch_capnproto.cpp create mode 100644 tests/capnproto/test_field_dispatch.cpp diff --git a/benchmarks/all/field_dispatch_capnproto.cpp b/benchmarks/all/field_dispatch_capnproto.cpp new file mode 100644 index 000000000..d934e4365 --- /dev/null +++ b/benchmarks/all/field_dispatch_capnproto.cpp @@ -0,0 +1,21 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_capnproto(benchmark::State& state) { + const auto schema = rfl::capnproto::to_schema>(); + run( + state, + [&](const auto& input) { return rfl::capnproto::write(input, schema); }, + [&](const auto& bytes) { + return rfl::capnproto::read>(bytes, schema); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_capnproto, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_capnproto, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/capnproto/Reader.hpp b/include/rfl/capnproto/Reader.hpp index eda33add4..ac4956d3c 100644 --- a/include/rfl/capnproto/Reader.hpp +++ b/include/rfl/capnproto/Reader.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../Bytestring.hpp" #include "../Result.hpp" @@ -178,8 +179,21 @@ class RFL_API Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { + const auto fields = _obj.val_.getSchema().getFields(); + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (fields.size() == ObjectReader::size()) { + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.val_.get(fields[Is])}), + ...); + }(std::make_integer_sequence{}); + return std::nullopt; + } + } int i = 0; - for (auto field : _obj.val_.getSchema().getFields()) { + for (auto field : fields) { _object_reader.read(i++, InputVarType{_obj.val_.get(field)}); } return std::nullopt; diff --git a/tests/capnproto/test_field_dispatch.cpp b/tests/capnproto/test_field_dispatch.cpp new file mode 100644 index 000000000..a343deb21 --- /dev/null +++ b/tests/capnproto/test_field_dispatch.cpp @@ -0,0 +1,26 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_capnproto = [](const auto& input) { + return rfl::capnproto::write(input); +}; +const auto read_capnproto = [](const auto& bytes) { + return rfl::capnproto::read(bytes); +}; +TEST(capnproto, field_dispatch_errors_and_cleanup) { + check_errors(write_capnproto, read_capnproto); +} +TEST(capnproto, field_dispatch_empty_object) { + check_empty(write_capnproto, read_capnproto); +} +TEST(capnproto, field_dispatch_schema_counts) { + const auto schema = rfl::capnproto::to_schema(); + const auto name = rfl::capnproto::get_root_name(); + const auto root_schema = schema.value().getNested(name.c_str()).asStruct(); + capnp::MallocMessageBuilder message; + const auto root = message.initRoot(root_schema); + const rfl::capnproto::Reader reader; + check_counts(reader, {root.asReader()}, 3); +} +} // namespace test_positional_field_dispatch From 867b18fb1d2fabe2eb60796da308123cf54b1751 Mon Sep 17 00:00:00 2001 From: Georgii Evmenov Date: Thu, 15 Oct 2026 03:57:19 +0300 Subject: [PATCH 6/6] Dispatch NoFieldNames values through indexed field readers Replace the per-element field scan with a constexpr table of field-specific readers, preserving bounds checks, defaults, validation, and cleanup. Cover supported serialization formats and Generic; benchmark JSON and MessagePack with and without DefaultIfMissing. --- .../all/field_dispatch_no_field_names.cpp | 57 +++++++++++++ ...ReaderWithDefaultAndStrippedFieldNames.hpp | 19 +++-- .../ViewReaderWithStrippedFieldNames.hpp | 20 +++-- .../test_no_field_names_dispatch.cpp | 14 ++++ tests/cbor/test_no_field_names_dispatch.cpp | 14 ++++ tests/cereal/test_no_field_names_dispatch.cpp | 14 ++++ tests/check_no_field_names_dispatch.hpp | 30 +++++++ .../test_no_field_names_dispatch.cpp | 14 ++++ .../generic/test_no_field_names_dispatch.cpp | 13 +++ tests/json/test_no_field_names_dispatch.cpp | 79 +++++++++++++++++++ .../msgpack/test_no_field_names_dispatch.cpp | 14 ++++ tests/ubjson/test_no_field_names_dispatch.cpp | 14 ++++ tests/yas/test_no_field_names_dispatch.cpp | 14 ++++ 13 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 benchmarks/all/field_dispatch_no_field_names.cpp create mode 100644 tests/boost_serialization/test_no_field_names_dispatch.cpp create mode 100644 tests/cbor/test_no_field_names_dispatch.cpp create mode 100644 tests/cereal/test_no_field_names_dispatch.cpp create mode 100644 tests/check_no_field_names_dispatch.hpp create mode 100644 tests/flexbuffers/test_no_field_names_dispatch.cpp create mode 100644 tests/generic/test_no_field_names_dispatch.cpp create mode 100644 tests/json/test_no_field_names_dispatch.cpp create mode 100644 tests/msgpack/test_no_field_names_dispatch.cpp create mode 100644 tests/ubjson/test_no_field_names_dispatch.cpp create mode 100644 tests/yas/test_no_field_names_dispatch.cpp diff --git a/benchmarks/all/field_dispatch_no_field_names.cpp b/benchmarks/all/field_dispatch_no_field_names.cpp new file mode 100644 index 000000000..9f0d89ebf --- /dev/null +++ b/benchmarks/all/field_dispatch_no_field_names.cpp @@ -0,0 +1,57 @@ +#include +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_json_no_field_names(benchmark::State& state) { + run( + state, + [](const auto& input) { + return rfl::json::write(input); + }, + [](const auto& bytes) { + return rfl::json::read, rfl::NoFieldNames, Ps...>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record2, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record16, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +template +void BM_field_dispatch_msgpack_no_field_names(benchmark::State& state) { + run( + state, + [](const auto& input) { + return rfl::msgpack::write(input); + }, + [](const auto& bytes) { + return rfl::msgpack::read, rfl::NoFieldNames, Ps...>( + bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record2, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record16, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index de57b25d3..e5d20e150 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -1,6 +1,7 @@ #ifndef RFL_PARSING_VIEWREADERWITHDEFAULTANDSTRIPPEDFIELDNAMES_HPP_ #define RFL_PARSING_VIEWREADERWITHDEFAULTANDSTRIPPEDFIELDNAMES_HPP_ +#include #include #include #include @@ -60,8 +61,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { << " fields, but got at least one more."; return Error(stream.str()); } - assign_to_field_i(*r_, _var, view_, errors_, i_, - std::make_integer_sequence()); + assign_to_field_i(_var, std::make_integer_sequence()); ++i_; return std::nullopt; } @@ -100,10 +100,17 @@ class ViewReaderWithDefaultAndStrippedFieldNames { } template - static void assign_to_field_i(const R& _r, const auto& _var, auto* _view, - auto* _errors, int _i, - std::integer_sequence) { - (assign_if_field_is_field_i(_r, _var, _view, _errors, _i), ...); + void assign_to_field_i(const InputVarType& _var, + std::integer_sequence) const { + using ReadField = void (*)( + const ViewReaderWithDefaultAndStrippedFieldNames*, const InputVarType&); + static constexpr std::array readers{ + +[](const ViewReaderWithDefaultAndStrippedFieldNames* self, + const InputVarType& var) { + assign_if_field_is_field_i(*self->r_, var, self->view_, + self->errors_, is); + }...}; + readers[i_](this, _var); } // TODO: Unnecessary code duplication. diff --git a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp index cc776bebc..160641341 100644 --- a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp @@ -59,8 +59,7 @@ class ViewReaderWithStrippedFieldNames { << " fields, but got at least one more."; return Error(stream.str()); } - assign_to_field_i(*r_, _var, view_, errors_, found_, set_, i_, - std::make_integer_sequence()); + assign_to_field_i(_var, std::make_integer_sequence()); ++i_; return std::nullopt; } @@ -102,11 +101,18 @@ class ViewReaderWithStrippedFieldNames { } template - static void assign_to_field_i(const R& _r, const auto& _var, auto* _view, - auto* _errors, auto* _found, auto* _set, int _i, - std::integer_sequence) { - (assign_if_field_is_field_i(_r, _var, _view, _errors, _found, _set, _i), - ...); + void assign_to_field_i(const InputVarType& _var, + std::integer_sequence) const { + using ReadField = + void (*)(const ViewReaderWithStrippedFieldNames*, const InputVarType&); + static constexpr std::array readers{ + +[](const ViewReaderWithStrippedFieldNames* self, + const InputVarType& var) { + assign_if_field_is_field_i(*self->r_, var, self->view_, + self->errors_, self->found_, + self->set_, is); + }...}; + readers[i_](this, _var); } // TODO: Unnecessary code duplication. diff --git a/tests/boost_serialization/test_no_field_names_dispatch.cpp b/tests/boost_serialization/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..e74824710 --- /dev/null +++ b/tests/boost_serialization/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(boost_serialization, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::boost_serialization::write(value); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/cbor/test_no_field_names_dispatch.cpp b/tests/cbor/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..31c6cd9ca --- /dev/null +++ b/tests/cbor/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(cbor, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::cbor::write(value); + }, + [](const auto& bytes) { + return rfl::cbor::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/cereal/test_no_field_names_dispatch.cpp b/tests/cereal/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..64c0d7477 --- /dev/null +++ b/tests/cereal/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(cereal, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::cereal::write(value); + }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/check_no_field_names_dispatch.hpp b/tests/check_no_field_names_dispatch.hpp new file mode 100644 index 000000000..1e3c8d2ad --- /dev/null +++ b/tests/check_no_field_names_dispatch.hpp @@ -0,0 +1,30 @@ +#ifndef CHECK_NO_FIELD_NAMES_DISPATCH_HPP_ +#define CHECK_NO_FIELD_NAMES_DISPATCH_HPP_ +#include + +#include +#include +#include + +namespace test_no_field_names_dispatch { +struct Record { + int first; + std::string middle; + int last; + bool operator==(const Record&) const = default; +}; +struct Data { + std::vector records; + bool operator==(const Data&) const = default; +}; +template +void check_roundtrip(Write write, Read read) { + const Data input{{{1, "first", 2}, {-3, "second", 4}}}; + const auto bytes = write(input); + const auto result = read.template operator()(bytes); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(*result, input); + EXPECT_EQ(write(*result), bytes); +} +} // namespace test_no_field_names_dispatch +#endif diff --git a/tests/flexbuffers/test_no_field_names_dispatch.cpp b/tests/flexbuffers/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..dc8ff4e5d --- /dev/null +++ b/tests/flexbuffers/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(flexbuffers, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::flexbuf::write(value); + }, + [](const auto& bytes) { + return rfl::flexbuf::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/generic/test_no_field_names_dispatch.cpp b/tests/generic/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..bc59ebaf0 --- /dev/null +++ b/tests/generic/test_no_field_names_dispatch.cpp @@ -0,0 +1,13 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" + +namespace test_no_field_names_dispatch { +TEST(generic, no_field_names_dispatch_roundtrip) { + const Data input{{{1, "first", 2}, {-3, "second", 4}}}; + const auto value = rfl::to_generic(input); + const auto result = rfl::from_generic(value); + ASSERT_TRUE(result); + EXPECT_EQ(*result, input); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/json/test_no_field_names_dispatch.cpp b/tests/json/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..d66ca1c36 --- /dev/null +++ b/tests/json/test_no_field_names_dispatch.cpp @@ -0,0 +1,79 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(json, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::json::write(value); + }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch + +#include "../check_positional_field_dispatch.hpp" + +namespace test_no_field_names_dispatch { +TEST(json, no_field_names_dispatch_errors_and_cleanup) { + test_positional_field_dispatch::check_errors( + [](const auto& value) { + return rfl::json::write(value); + }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} + +TEST(json, no_field_names_dispatch_missing_and_extra_fields) { + const auto missing = + rfl::json::read(R"([1,"middle"])"); + ASSERT_FALSE(missing); + EXPECT_NE( + std::string(missing.error().what()).find("Field named 'last' not found"), + std::string::npos); + const auto defaults = + rfl::json::read( + R"([1,"middle"])"); + ASSERT_TRUE(defaults); + EXPECT_EQ(defaults->last, 0); + const auto extra = + rfl::json::read(R"([1,"middle",2,3])"); + ASSERT_FALSE(extra); + EXPECT_NE( + std::string(extra.error().what()).find("Expected a maximum of 3 fields"), + std::string::npos); + const auto extra_defaults = + rfl::json::read( + R"([1,"middle",2,3])"); + ASSERT_FALSE(extra_defaults); + EXPECT_EQ(extra_defaults.error().what(), extra.error().what()); +} + +struct Defaults { + rfl::Validator> first = 0; + std::string middle = "default"; + rfl::Validator> last = 0; +}; +TEST(json, no_field_names_dispatch_default_errors) { + const auto result = + rfl::json::read( + R"([-1,"middle",-2])"); + ASSERT_FALSE(result); + const std::string message = result.error().what(); + EXPECT_NE(message.find("Found 2 errors:"), std::string::npos); + EXPECT_LT(message.find("Failed to parse field 'first'"), + message.find("Failed to parse field 'last'")); +} + +struct Empty {}; +TEST(json, no_field_names_dispatch_empty) { + EXPECT_TRUE((rfl::json::read("[]"))); + EXPECT_FALSE((rfl::json::read("[1]"))); + EXPECT_TRUE( + (rfl::json::read("[]"))); + EXPECT_FALSE(( + rfl::json::read("[1]"))); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/msgpack/test_no_field_names_dispatch.cpp b/tests/msgpack/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..b70482dcf --- /dev/null +++ b/tests/msgpack/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(msgpack, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::msgpack::write(value); + }, + [](const auto& bytes) { + return rfl::msgpack::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/ubjson/test_no_field_names_dispatch.cpp b/tests/ubjson/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..391a917c6 --- /dev/null +++ b/tests/ubjson/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(ubjson, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::ubjson::write(value); + }, + [](const auto& bytes) { + return rfl::ubjson::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/yas/test_no_field_names_dispatch.cpp b/tests/yas/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..d3ef82ca9 --- /dev/null +++ b/tests/yas/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(yas, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::yas::write(value); + }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch