arena: make MapView::find behave like std::map, and document the surface it lives in - #48
Merged
Conversation
…ace it lives in
`MapView::find` returned `nullptr` on a miss while `end()` was one-past-the-end, so
`find(k) != end()` -- the spelling every std::map user reaches for -- compiled clean
under -Wall -Wextra, was true for a MISS on a non-empty map, and then dereferenced
null. On a map with no entries it was accidentally false, so it passed a unit test
and crashed on the first message that had any.
find() now returns an iterator and compares against end(). Returning end() from the
old pointer type would have fixed that spelling and broken `if (auto* e = find(k))`
in exactly the same silent way, since end() is non-null on a non-empty map, so the
iterator converts to neither a pointer nor bool: both old spellings are compile
errors now, and no idiom is left that quietly returns the wrong answer. It is a
forward iterator, so range-for, std::distance and the algorithms work as before, and
MapView keeps its 12-byte cell. The break found five call sites in this repo's own
tests, which is the argument for making it loud.
The docs work that surfaced it is here too. docs/arena.md taught the accessor surface
through a hand-written schema, then walked a googleapis message where nearly every
accessor is std::optional -- but that schema is an outlier (54% optional against 6%
across sixty other googleapis schemas), and neither of the two compiler traps it
demonstrated has anything to do with optional: ArrayView yielding values and MapView
entries being class types fire on any schema. So the worked example now reads a
Person carrying one of each shape, where one line in seven is an optional, and says
what actually governs presence: a message field is a pointer whether or not it says
`optional`, a scalar becomes one only when the schema asks.
The rest is failures that are silent, each executed, each placed where the reader
already is:
- ByteView is std::string_view, so `decode(buf.substr(4), arena)` binds a temporary
and dangles although the caller kept `buf` alive as instructed -- and the doc now
says why the two spellings differ, since substr returns a new std::string
- reset() said every POINTER dangles, so keeping only values felt safe;
ArrayView/MapView/StringArrayView and node copies hold arena pointers too, and
reset() frees nothing, so stale reads look right until the next decode
- the error struct is written only on failure and never cleared, so one hoisted out
of a loop reports 1 ok / 3 failed where the truth is 3 ok / 1 failed
- a string_view field is not NUL-terminated and a bytes value may contain NULs:
strcmp(blob.data(), "A") == 0 is true for a blob of A\0B\0C
- decode_owned cannot bound memory, and dies at the end of a range-init
Corrected: RepeatedSingularMessage covers a map entry repeating its value only for a
MESSAGE value; a proto2 required sub-message pointer is never null; nested groups
report Wire/GroupTooDeep; `optional bytes` also returns std::optional; escaping
applies to messages, enums and package components. README's "misuse ... is a compile
error, not a silent bug" no longer claims more than holds.
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.
MapView::findreturnednullptron a miss whileend()was one-past-the-end, sofind(k) != end()-- the spelling every std::map user reaches for -- compiled clean under -Wall -Wextra, was true for a MISS on a non-empty map, and then dereferenced null. On a map with no entries it was accidentally false, so it passed a unit test and crashed on the first message that had any.find() now returns an iterator and compares against end(). Returning end() from the old pointer type would have fixed that spelling and broken
if (auto* e = find(k))in exactly the same silent way, since end() is non-null on a non-empty map, so the iterator converts to neither a pointer nor bool: both old spellings are compile errors now, and no idiom is left that quietly returns the wrong answer. It is a forward iterator, so range-for, std::distance and the algorithms work as before, and MapView keeps its 12-byte cell. The break found five call sites in this repo's own tests, which is the argument for making it loud.The docs work that surfaced it is here too. docs/arena.md taught the accessor surface through a hand-written schema, then walked a googleapis message where nearly every accessor is std::optional -- but that schema is an outlier (54% optional against 6% across sixty other googleapis schemas), and neither of the two compiler traps it demonstrated has anything to do with optional: ArrayView yielding values and MapView entries being class types fire on any schema. So the worked example now reads a Person carrying one of each shape, where one line in seven is an optional, and says what actually governs presence: a message field is a pointer whether or not it says
optional, a scalar becomes one only when the schema asks.The rest is failures that are silent, each executed, each placed where the reader already is:
decode(buf.substr(4), arena)binds a temporary and dangles although the caller keptbufalive as instructed -- and the doc now says why the two spellings differ, since substr returns a new std::stringCorrected: RepeatedSingularMessage covers a map entry repeating its value only for a MESSAGE value; a proto2 required sub-message pointer is never null; nested groups report Wire/GroupTooDeep;
optional bytesalso returns std::optional; escaping applies to messages, enums and package components. README's "misuse ... is a compile error, not a silent bug" no longer claims more than holds.