Repository navigation
Avoid needless copies, allocations and exceptions when reading and writing - #767
Open
clockblocker wants to merge 5 commits into
Open
clockblocker wants to merge 5 commits into
clockblocker wants to merge 5 commits into
Conversation
Validating a value copied it several times: once in each step of AllOf, once in Size and PatternValidator, and once more because Validator(T&&) did not move its argument. Validating a string with two Size constraints made four extra copies of it. The value is now moved through the validators instead. Size and AllOf return early on failure rather than chaining transform and and_then. The chains create more Result temporaries than the copies they would save, which made short strings slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When reading, a failed validation made the Validator constructor throw and the parser catch the exception, which costs several microseconds. The parser now uses Validator::from_value, which returns the error directly. from_value gets an overload for rvalues and no longer throws internally. The error messages stay the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading an untagged variant reserved space for an error message per alternative before trying any of them, so every read allocated, even when the first alternative matched. The space is now reserved when the first alternative fails. The error messages of failed alternatives are also moved instead of copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When reading, a string that is not one of the literal's values made the Literal constructor throw and the parser catch the exception. The parser now uses Literal::from_string, which returns the error directly. The error message stays the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When writing a tagged union, wrap_if_necessary returned an alternative that already contains the discriminator field by value, which copied the whole object on every write. Such alternatives are now written directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clockblocker
force-pushed
the
validators-avoid-copies
branch
from
November 3, 2026 16:02
d2231dc to
552b1b6
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Five pessimisations on the read and write paths, one commit each. Error messages are unchanged.
Validator(T&&), everyAllOfstep,SizeandPatternValidatorcopied the valueSizeandAllOfinstead of chainingtransform/and_thenValidatorconstructor and was caught in the parserValidator::from_value, which returns the error directlyLiteralconstructor and was caught in the parserLiteral::from_stringwrap_if_necessaryreturned such alternatives by valueWhole PR,
main→552b1b6:test_size)test_email)test_all_of)test_all_of)std::variant, 1st alternative (test_variant)std::variant, 2nd alternative (test_variant)rfl::Variant, 1st alternative (test_rfl_variant)std::variant, no alternative matches (test_variant)test_literal)"Ned"fails (test_literal)test_tagged_union3)1. Move values through validators instead of copying them
The early returns matter. Moving the value while keeping the
transform/and_thenchains made short strings 60–75% slower, because each step creates anotherResult.test_size)test_email)test_all_of)test_validator_copies)2. Read validators without throwing an exception on failure
Validator::from_valuegets aT&&overload. Both overloads callvalidateand wrap the result instead of constructing aValidatorand catching its exception. Both keep thetry/catch, so a custom validator that throws is still handled.test_all_of)test_all_of)test_email)from_value(std::move(v))with 3 elements (test_validator_copies)3. Do not allocate when an untagged variant matches
ParserRflVariantalready hadstd::move(res.error().what()), but that moves aconst std::string&, so it still copied.std::variant, 1st alternative (test_variant)std::variant, 2nd alternative (test_variant)rfl::Variant, 1st alternative (test_rfl_variant)std::variant, no alternative matches (test_variant)4. Read literals without throwing an exception on failure
"Ned"fails (test_literal)test_literal)5. Do not copy tagged union alternatives that contain the tag when writing
test_tagged_union3)test_tagged_union_copies)The
test_tagged_union3alternatives hold a literal and two doubles, so the copy was cheap. For alternatives with strings or containers it was a deep copy on every write.Evidence
New tests that fail on
mainand pass on this branch:test_validator_copies_nothing_when_reading: 8 → 0 copiestest_validator_copies_nothing_when_moved_in: 12 → 0 copiestest_tagged_union_copies_nothing_when_writing: 1 → 0 copiestest_validator_reports_error_when_readingandtest_literal_reports_unsupported_stringpin the exact error messages. They pass before and after.Each commit compiles, and passes the tests it adds. On the final commit, local runs (Apple clang 17,
REFLECTCPP_USE_VCPKG=OFF) give JSON 270, CLI 71, generic 98, env 41, json_c_arrays_and_inheritance 7, all passing. The touched tests also compile with C++23 andREFLECTCPP_USE_STD_EXPECTED.Benchmark method: the types and inputs are copied from the tests named in each row.
benchmarks/has no validators, untagged variants or tagged unions. Allocations are counted by replacingoperator newfor one call. Time is the median of 3 interleaved runs, each the median of 15 rounds. Apple clang 17,-O2.Benchmark source
Merge Danger
Door: two-way. Header-only changes, nothing persisted.
Blast Radius: small.
Size::validateandPatternValidator::validatenow take their argument by value. Calls keep compiling, but taking their address as a function pointer of the old type would not.Validator::from_value(T&&)is a new overload. A call with an rvalue now picks it, with the same result.AnyOfandOneOfstill copy once per alternative, because every alternative has to see the original value. Changing that would change the validator interface, so it's not part of this PR.🤖 Generated with Claude Code