Skip to content

arena: make MapView::find behave like std::map, and document the surface it lives in - #48

Merged
VeaaC merged 1 commit into
mainfrom
arena-docs-2.8
Aug 15, 2026
Merged

arena: make MapView::find behave like std::map, and document the surface it lives in#48
VeaaC merged 1 commit into
mainfrom
arena-docs-2.8

Conversation

@VeaaC

@VeaaC VeaaC commented Aug 15, 2026

Copy link
Copy Markdown
Owner

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.

…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.
@VeaaC
VeaaC merged commit 975c074 into main Aug 15, 2026
9 checks passed
@VeaaC
VeaaC deleted the arena-docs-2.8 branch August 15, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant