<xmemory>: Document the iterator debugging invariants - #6391
<xmemory>: Document the iterator debugging invariants#6391Prakriti Sharma (prakriti31) wants to merge 11 commits into
<xmemory>: Document the iterator debugging invariants#6391Conversation
Fixes microsoftGH-2084. Captures the IDL proxy/iterator invariants (explained by @StephanTLavavej in the issue thread) as a comment next to the _Container_proxy/_Container_base12/_Iterator_base12 machinery, since they were previously only recorded in a Discord screenshot linked from the issue.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Documents iterator-debugging proxy relationships, iterator lists, and locking behavior in <xmemory>.
Changes:
- Adds comments describing iterator-debugging invariants.
- Introduces no behavioral changes.
Suppressed comments (2)
stl/inc/xmemory:1222
- This invariant only holds when
_ITERATOR_DEBUG_LEVEL == 2. At level 1,_Iterator_base12::_Adopt(lines 1327–1333) copies_Myproxybut never links the iterator into_Myfirstiter, so valid iterators are deliberately absent from this list. Please qualify the invariant so the documentation matches both_Container_base12modes.
// * The proxy's _Myfirstiter, together with each iterator's _Mynextiter, forms an intrusive singly linked list of
// iterators rooted at the proxy. Every valid iterator belonging to a container is reachable through this list;
// there are no valid "free-floating" iterators.
stl/inc/xmemory:1226
- The lock rule is not unconditional: level-1 proxy swaps use the unlocked helper (lines 1457–1459), and level-2 constant evaluation also invokes unlocked helpers. Runtime list mutations at level 2 are locked, but proxy allocation/reloading/deletion is not. Please state that narrower invariant instead of saying every proxy manipulation holds
_LOCK_DEBUG.
// * Whenever the proxies and the intrusive list are manipulated, the debug lock (_Lockit(_LOCK_DEBUG)) is held.
// The only things we do outside of that lock are things like iterator compatibility checks that compare proxy
// pointers: those pointers don't change even if the containers are being swapped concurrently (only the
// proxies' data members change, not their addresses).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@microsoft-github-policy-service agree |
|
Stephan T. Lavavej (@StephanTLavavej) What else do we need to merge it. Can you help me? I will push the changes asap. |
|
Someone who is deeply familiar with the invariants (i.e. me) needs to review this and think about whether the description is correct and complete. I have a high-priority task to get through ( |
|
Sure!! |
- Clarify that "IDL" refers to _ITERATOR_DEBUG_LEVEL ("level", not
"library"), per frederick-vs-ja's review comment.
- Explain that the separately-allocated _Container_proxy (TRANSITION,
ABI) is why several containers' allocator-extended move operations
aren't unconditionally noexcept, and link to microsoft#169 for the vNext plan.
- Note that the debug lock is skipped during constant evaluation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (6)
stl/inc/xmemory:1232
- The claim about containers being swapped concurrently reads like a thread-safety guarantee, but
swapracing with iterator operations is still undefined behavior and can involve data races even if proxy addresses are stable. Consider rewording to avoid implying concurrency safety (e.g., focus on the intended invariant: proxy objects aren’t relocated, so address comparisons remain meaningful without taking the debug lock).
// isn't usable there. The only other things we do outside of the lock at runtime are things like iterator
// compatibility checks that compare proxy pointers: those pointers don't change even if the containers are
// being swapped concurrently (only the proxies' data members change, not their addresses).
stl/inc/xmemory:1215
- This bullet is phrased as applying to 'Every container', but the machinery here is specifically about containers participating in IDL (i.e., those using
_Container_base12/ the proxy mechanism). To prevent readers from overgeneralizing, consider scoping the statement (e.g., 'Every IDL-enabled container' or 'Every container derived from_Container_base12').
// * Every container owns a dynamically allocated _Container_proxy at all times, including in its
stl/inc/xmemory:1216
- The inline 'This is TRANSITION, ABI:' reads awkwardly and is inconsistent with typical tag-style notes. Consider restructuring this into a standalone
// TRANSITION, ABI:line (or rephrasing the sentence) so the rationale is clearer and the tag is easier to scan.
// default-constructed and moved-from states. This is TRANSITION, ABI: allocating the proxy separately (instead
stl/inc/xmemory:1211
- Several of these comment lines are quite long. If this repo follows a column limit for headers, wrapping these lines would improve readability and keep formatting consistent with surrounding code/comments.
// The machinery below implements iterator debugging (informally "IDL", after the _ITERATOR_DEBUG_LEVEL macro,
stl/inc/xmemory:1217
- Several of these comment lines are quite long. If this repo follows a column limit for headers, wrapping these lines would improve readability and keep formatting consistent with surrounding code/comments.
// of, say, storing it inline) is the major reason many containers' allocator-extended move constructors and
stl/inc/xmemory:1227
- Several of these comment lines are quite long. If this repo follows a column limit for headers, wrapping these lines would improve readability and keep formatting consistent with surrounding code/comments.
// * The proxy's _Myfirstiter, together with each iterator's _Mynextiter, forms an intrusive singly linked list of
// iterators rooted at the proxy. Every valid iterator belonging to a container is reachable through this list;
// there are no valid "free-floating" iterators.
// * Whenever the proxies and the intrusive list are manipulated at runtime, the debug lock (_Lockit(_LOCK_DEBUG))
- Note that _ITERATOR_DEBUG_LEVEL == 0 uses _Container_base0 and _Fake_proxy_ptr_impl, so no proxy is allocated at all, per Copilot's review comment (endorsed by frederick-vs-ja). - Separate the invariants holding at levels 1 and 2 from those holding only at level 2. The _Myfirstiter/_Mynextiter list is level-2 only: at level 1, _Adopt merely copies _Myproxy, _Orphan_all is empty, and _Myfirstiter stays null. - Correct the locking bullet. The debug lock guards the iterator list, not the proxy pointers: _Alloc_proxy and _Reload_proxy install a new proxy without taking it at either level, and at level 1 _Swap_proxy_and_iterators calls the unlocked helper directly. - Refer to microsoftGH-169 rather than microsoft#169, matching the other headers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The documented invariants need correction before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
stl/inc/xmemory:1228
- The list and locking invariants apply only at IDL 2. At IDL 1,
_Adoptmerely copies_Myproxy(xmemory:1332-1339) and proxy swapping takes the unlocked branch (xmemory:1463-1465);_Alloc_proxyand_Reload_proxyalso mutate proxy state without locking. Please qualify list membership to IDL 2 and narrow the lock claim to the operations that actually take it.
// * The proxy's _Myfirstiter, together with each iterator's _Mynextiter, forms an intrusive singly linked list of
// iterators rooted at the proxy. Every valid iterator belonging to a container is reachable through this list;
// there are no valid "free-floating" iterators.
// * Whenever the proxies and the intrusive list are manipulated at runtime, the debug lock (_Lockit(_LOCK_DEBUG))
// is held. During constant evaluation, we skip the lock entirely and go straight to the unlocked paths (see the
stl/inc/xmemory:1221
- The “always”/“regardless” claim is inaccurate at level 0, where
_Container_base0has no_Myproxyand no proxy is allocated. It also overlooks intentionally unbound proxies created by_Container_proxy_ptr12(..., _Leave_proxy_unbound), so state this as an invariant of bound proxies when IDL is enabled.
// * A container and its proxy always point to each other (_Container_base12::_Myproxy and
// _Container_proxy::_Mycont, respectively), regardless of whether IDL is enabled; if a proxy exists, this holds.
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
The unresolved deque documentation accuracy concern warrants final human review.
Review details
Suppressed comments (1)
stl/inc/xmemory:1215
- This overlooks
deque:_Deque_valdeliberately derives from_Container_base12unconditionally anddequeallocates_Container_proxyobjects even when_ITERATOR_DEBUG_LEVEL == 0(stl/inc/deque:533andstl/inc/deque:643-645). As written, the new overview gives an incorrect level-0 invariant; please document the ABI-preserving exception.
// needing to know about the other directly, by routing everything through a shared proxy object. Only containers
// with _ITERATOR_DEBUG_LEVEL != 0 use it; at level 0 they derive from _Container_base0 and use
// _Fake_proxy_ptr_impl instead, and no proxy is allocated at all.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
deque is an exception to the "level 0 means no proxy" rule: _Deque_val derives from _Container_base12 and _Deque_const_iterator from _Iterator_base12 at every _ITERATOR_DEBUG_LEVEL, because deque's offset-based iterators reach the container through the proxy (_Getcont(), used by the unguarded _Unwrapped()) even when iterator debugging is off. A deque therefore allocates a real _Container_proxy at level 0 too. Also frame the first group of invariants as applying to any container that uses _Container_base12, rather than to levels 1 and 2; note that _Container_proxy_ptr12 can briefly hold a not-yet-bound proxy (see _Leave_proxy_unbound); and say "below level 2" where the text previously said "level 1". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The only unresolved comment is a minor documentation clarification that does not block approval.
Review details
Suppressed comments (1)
stl/inc/xmemory:1248
- Clarify which pointers remain stable during a swap.
_Swap_proxy_and_iterators_unlockedswaps each container's_Myproxyvalue (xmemory:1455-1458), so the current broad statement that “proxy pointers” do not change can be read as contradicting the implementation. It is specifically existing iterators'_Myproxyvalues and the proxy object addresses that remain unchanged; each proxy's_Mycontis then updated.
// directly. The other things we do outside the lock are things like iterator compatibility checks that compare
// proxy pointers: those pointers don't change when containers are swapped (only the proxies' data members
// change, not their addresses).
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
The documented invariants contain three inaccuracies that should be corrected.
Review details
Suppressed comments (3)
stl/inc/xmemory:1232
- This invariant is not true below level 2. At levels 0 and 1,
_Container_base12::_Orphan_all()is a no-op, so an invalidated iterator can retain a non-null (and after destruction, dangling)_Myproxy. Since the introduction uses “orphan” as a synonym for invalidation, please restrict the nulling guarantee to level 2.
// * Every valid iterator holds a non-owning pointer to its parent container's proxy (_Iterator_base12::_Myproxy);
// an orphaned iterator has a null _Myproxy.
stl/inc/xmemory:1248
- This conflates three different pointers. Compatibility checks compare container addresses returned by
_Getcont()(for example,deque:388), while_Swap_proxy_and_iterators_unlocked()exchanges the containers'_Myproxyvalues and rewrites each proxy's_Mycont; only existing iterators'_Myproxyvalues remain unchanged. Please state that distinction explicitly so the documented swap invariant matches the implementation.
// directly. The other things we do outside the lock are things like iterator compatibility checks that compare
// proxy pointers: those pointers don't change when containers are swapped (only the proxies' data members
// change, not their addresses).
stl/inc/xmemory:1227
- The noexcept rationale does not match the implementations. For example,
vector(vector&&, const Alloc&)allocates its proxy even on itsnoexcept(is_always_equal)path (vector:770-777), while the_Reload_proxybranch in move assignment is explicitly inside a noexcept path and terminates on allocation failure (vector:800-824). The potentially throwing unequal, non-propagating path moves elements and does not reload the proxy. Please describe the ABI/allocation constraint without claiming that proxy reload determines these noexcept specifications.
// default-constructed and moved-from states. This is TRANSITION, ABI: allocating the proxy separately (instead
// of, say, storing it inline) is the major reason many containers' allocator-extended move constructors and
// move assignment operators aren't unconditionally noexcept -- reloading the proxy when allocators compare
// unequal can throw. We intend to revisit this strategy in vNext (see GH-169).
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
- Orphaning nulls _Myproxy, but only level 2 orphans anything: _Container_base12::_Orphan_all has an empty body below level 2, so an invalidated iterator there keeps its now-dangling _Myproxy. - Drop the claim that reloading the proxy is why containers' move operations aren't unconditionally noexcept; it has the causality backwards. vector(vector&&) is unconditionally noexcept yet calls _Alloc_proxy, the allocator-extended constructor allocates its proxy on both paths, and the _Reload_proxy call in move assignment sits inside a noexcept branch already tagged "intentionally slams into noexcept on OOM, TRANSITION, VSO-466800". The separate allocation forces termination on OOM rather than weakening the specifications, so say that instead. - Describe the swap accurately: compatibility checks compare the container addresses _Getcont() reads out of the proxies, and swapping exchanges the containers' _Myproxy values and rewrites each proxy's _Mycont, while existing iterators keep pointing at the same proxy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Correct the inaccurate _Myproxy invalidation invariant before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The malformed and duplicated comment text should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The documentation block is corrupted and redundant, requiring one clean replacement paragraph.
Review details
Suppressed comments (1)
stl/inc/xmemory:1237
- The new invariant paragraph contains malformed line endings after the first two continuation lines and then repeats “becomes dangling only when the proxy itself is destroyed.” This leaves the documentation block corrupted/redundant; replace the paragraph with one clean copy.
// Orphaning an iterator nulls that pointer, but only level 2 tracks invalidation and can null iterators through
// _Orphan_all. Below it, _Orphan_all is a no-op, so an invalidated iterator retains its _Myproxy; that pointer
// becomes dangling only when the proxy itself is destroyed.
// becomes dangling only when the proxy itself is destroyed.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The autofix commits left the orphaning bullet with its last line duplicated and two bare blank lines breaking the comment block. Collapse it back to a single paragraph. Keeps the autofix's more precise wording: an invalidated iterator below level 2 retains its _Myproxy, and that pointer dangles only once the proxy itself is destroyed, rather than immediately upon invalidation. Line endings were not affected; the file is still uniformly CRLF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The orphaning and free-floating iterator claims need qualification.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
stl/inc/xmemory:1241
- “There are no valid free-floating iterators” is not an invariant of all users of this machinery. A default or exhausted
regex_iteratoris a valid end-of-sequence iterator with no proxy (stl/inc/regex:2965,3037, and3058). Please scope the assertion to iterators that are currently associated with a container.
// * The proxy's _Myfirstiter, together with each iterator's _Mynextiter, forms an intrusive singly linked list of
// iterators rooted at the proxy. Every valid iterator belonging to a container is reachable through this list;
// there are no valid "free-floating" iterators. Below level 2 the list is unused: _Adopt merely copies
// _Myproxy, _Orphan_all does nothing, and _Myfirstiter remains null.
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The malformed line endings must be normalized before approval.
Review details
Suppressed comments (1)
stl/inc/xmemory:1235
- These four lines use
CRCRLFendings, unlike the surrounding single-CRLFlines (for example, lines 1229–1231). The extra carriage returns appear as embedded control characters and can confuse line-based formatting checks; normalize each ending to one CRLF.
// * Every iterator associated with a parent container holds a non-owning pointer to that container's proxy
// (_Iterator_base12::_Myproxy). Container-driven orphaning nulls that pointer, but _Orphan_all only does so at
// level 2. Below level 2, an iterator invalidated by its container retains its _Myproxy, which dangles only once
// the proxy itself is destroyed.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
A valid iterator can legitimately have a null _Myproxy, so the blanket assertion was wrong. An end-of-sequence regex_iterator reaches that state three ways: default construction, a failed initial search that never calls _Adopt, and exhaustion via an explicit _Adopt(nullptr) (regex:2965, 2972-2973, 3037, and 3058). Say that a null _Myproxy means no current association with a container, rather than that no such valid iterator exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The inconsistent line endings should be corrected before approval.
Review details
Suppressed comments (1)
stl/inc/xmemory:1235
- These four lines use
CRCRLFendings while the rest of this file usesCRLF, leaving an extra carriage-return byte on each line (and likely triggering whitespace checks). Please normalize these lines to the file's existing line endings.
// * Every iterator associated with a parent container holds a non-owning pointer to that container's proxy
// (_Iterator_base12::_Myproxy). Container-driven orphaning nulls that pointer, but _Orphan_all only does so at
// level 2. Below level 2, an iterator invalidated by its container retains its _Myproxy, which dangles only once
// the proxy itself is destroyed.
- Files reviewed: 1/1 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Closes #2084.
The iterator debugging (IDL) proxy/iterator machinery in
<xmemory>(
_Container_proxy,_Container_base12,_Iterator_base12) wasn'tcommented at all, even though its invariants are non-obvious (intrusive
linked list of iterators, mutual container/proxy pointers, locking rules).
Stephan T. Lavavej (@StephanTLavavej) explained these invariants in the issue thread (originally
via a linked Discord screenshot from 2021), so this PR just captures that
explanation as a comment next to the code it describes, so it doesn't only
live in a screenshot linked from a 4-year-old issue comment.
I verified the described invariants (mutual _Myproxy/_Mycont pointers, the
_Myfirstiter/_Mynextiter intrusive list, the _Lockit(_LOCK_DEBUG) locking
rule) still match the current code exactly before writing this up.
No behavior change -- comment-only.