Skip to content

Commit 65d5b1a

Browse files
committed
New issue from Hewill: "The past end issue for lazy_split_view"
1 parent 88cd29b commit 65d5b1a

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

xml/issue4249.xml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?xml version='1.0' encoding='utf-8' standalone='no'?>
2+
<!DOCTYPE issue SYSTEM "lwg-issue.dtd">
3+
4+
<issue num="4249" status="New">
5+
<title>The past end issue for `lazy_split_view`</title>
6+
<section>
7+
<sref ref="[range.lazy.split.outer]"/>
8+
</section>
9+
<submitter>Hewill Kang</submitter>
10+
<date>26 Apr 2025</date>
11+
<priority>99</priority>
12+
13+
<discussion>
14+
<p>
15+
Consider (<a href="https://godbolt.org/z/eeMe8aTqv">demo</a>):
16+
</p>
17+
<blockquote><pre>
18+
#include &lt;print&gt;
19+
#include &lt;ranges&gt;
20+
#include &lt;sstream&gt;
21+
22+
int main() {
23+
std::istringstream is{"1 0 2 0 3"};
24+
auto r = std::views::istream&lt;int&gt;(is)
25+
| std::views::lazy_split(0)
26+
| std::views::stride(2);
27+
std::println("{}", r); // should print [[1], [3]]
28+
}
29+
</pre></blockquote>
30+
<p>
31+
The above leads to SIGSEGV in libstdc++, the reason is that we are iterating over the nested
32+
range as:
33+
</p>
34+
<blockquote><pre>
35+
for (auto&amp;&amp; inner : r) {
36+
for (auto&amp;&amp; elem : inner) {
37+
// [&hellip;]
38+
}
39+
}
40+
</pre></blockquote>
41+
<p>
42+
which is disassembled as:
43+
</p>
44+
<blockquote><pre>
45+
auto outer_it = r.begin();
46+
std::default_sentinel_t out_end = r.end();
47+
for(; outer_it != out_end; ++outer_it) {
48+
auto&amp;&amp; inner_r = *outer_it;
49+
auto inner_it = inner_r.begin();
50+
std::default_sentinel_t inner_end = inner_r.end();
51+
for(; inner_it != inner_end; ++inner_it) {
52+
auto&amp;&amp; elem = *inner_it;
53+
// [&hellip;]
54+
}
55+
}
56+
</pre></blockquote>
57+
<p>
58+
Since `inner_it` and `output_it` actually update the same iterator,
59+
when we back to the outer loop, <code>lazy_split_view::<i>outer-iterator</i></code>
60+
is now equal to `default_sentinel`, which makes `output_it` reach the end,
61+
so `++outer_it` will increment the iterator past end, triggering the assertion.
62+
</p>
63+
<p>
64+
Note that this <a href="https://godbolt.org/z/6Tx61eeq8">also happens</a> in MSVC-STL
65+
when `_ITERATOR_DEBUG_LEVEL` is turned on.
66+
</p>
67+
<p>
68+
It seems that extra flags are needed to fix this issue because `output_it` should not
69+
be considered to reach the end when we back to the outer loop.
70+
</p>
71+
</discussion>
72+
73+
<resolution>
74+
<p>
75+
This wording is relative to <paper num="N5008"/>.
76+
</p>
77+
78+
<ol>
79+
80+
<li><p>Modify <sref ref="[range.lazy.split.outer]"/> as indicated:</p>
81+
82+
<blockquote>
83+
<blockquote>
84+
<pre>
85+
namespace std::ranges {
86+
template&lt;input_range V, forward_range Pattern&gt;
87+
requires view&lt;V&gt; &amp;&amp; view&lt;Pattern&gt; &amp;&amp;
88+
indirectly_comparable&lt;iterator_t&lt;V&gt;, iterator_t&lt;Pattern&gt;, ranges::equal_to&gt; &amp;&amp;
89+
(forward_range&lt;V&gt; || <i>tiny-range</i>&lt;Pattern&gt;)
90+
template&lt;bool Const&gt;
91+
struct lazy_split_view&lt;V, Pattern&gt;::<i>outer-iterator</i> {
92+
private:
93+
using <i>Parent</i> = <i>maybe-const</i>&lt;Const, lazy_split_view&gt;; // <i>exposition only</i>
94+
using <i>Base</i> = <i>maybe-const</i>&lt;Const, V&gt;; // <i>exposition only</i>
95+
<i>Parent</i>* <i>parent_</i> = nullptr; // <i>exposition only</i>
96+
97+
iterator_t&lt;<i>Base</i>&gt; <i>current_</i> = iterator_t&lt;<i>Base</i>&gt;(); // <i>exposition only, present only</i>
98+
// <i>if V models forward_range</i>
99+
100+
bool <i>trailing_empty_</i> = false; // <i>exposition only</i>
101+
<ins>bool <i>has_next_</i> = false; // <i>exposition only, present only</i></ins>
102+
<ins>// <i>if forward_range&lt;<i>V</i>&gt; is false</i></ins>
103+
public:
104+
[&hellip;]
105+
};
106+
}
107+
</pre>
108+
</blockquote>
109+
[&hellip;]
110+
<pre>
111+
constexpr explicit <i>outer-iterator</i>(<i>Parent</i>&amp; parent)
112+
requires (!forward_range&lt;<i>Base</i>&gt;);
113+
</pre>
114+
<blockquote>
115+
<p>
116+
-2- <i>Effects:</i> Initializes <code><i>parent_</i></code> with `addressof(parent)` <ins>and
117+
<code><i>has_next_</i></code> with <code><i>current</i> != ranges::end(<i>parent_</i>-&gt;<i>base_</i>)</code></ins>.
118+
</p>
119+
</blockquote>
120+
[&hellip;]
121+
<pre>
122+
constexpr <i>outer-iterator</i>&amp; operator++();
123+
</pre>
124+
<blockquote>
125+
<p>
126+
-6- <i>Effects:</i> Equivalent to:
127+
</p>
128+
<blockquote><pre>
129+
const auto end = ranges::end(<i>parent_</i>-&gt;<i>base_</i>);
130+
if (<i>current</i> == end) {
131+
<i>trailing_empty_</i> = false;
132+
<ins>if constexpr (!forward_range&lt;V&gt;)
133+
<i>has_next_</i> = false;</ins>
134+
return <code>*this</code>;
135+
}
136+
const auto [pbegin, pend] = subrange{<i>parent_</i>-&gt;<i>pattern_</i>};
137+
if (pbegin == pend) ++<i>current</i>;
138+
else if constexpr (<i>tiny-range</i>&lt;Pattern&gt;) {
139+
<i>current</i> = ranges::find(std::move(<i>current</i>), end, *pbegin);
140+
if (<i>current</i> != end) {
141+
++<i>current</i>;
142+
if (<i>current</i> == end)
143+
<i>trailing_empty_</i> = true;
144+
}
145+
}
146+
else {
147+
do {
148+
auto [b, p] = ranges::mismatch(<i>current</i>, end, pbegin, pend);
149+
if (p == pend) {
150+
<i>current</i> = b;
151+
if (<i>current</i> == end)
152+
<i>trailing_empty_</i> = true;
153+
break; <i>// The pattern matched; skip it</i>
154+
}
155+
} while (++<i>current</i> != end);
156+
}
157+
<ins>if constexpr (!forward_range&lt;V&gt;)
158+
if (<i>current</i> == end)
159+
<i>has_next_</i> = false;</ins>
160+
return *this;
161+
</pre></blockquote>
162+
</blockquote>
163+
[&hellip;]
164+
<pre>
165+
friend constexpr bool operator==(const <i>outer-iterator</i>&amp; x, default_sentinel_t);
166+
</pre>
167+
<blockquote>
168+
<p>
169+
-8- <i>Effects:</i> Equivalent to:
170+
</p>
171+
<blockquote><pre>
172+
<ins>if constexpr (!forward_range&lt;V&gt;)
173+
return !x.<i>has_next_</i> &amp;&amp; !x.<i>trailing_empty_</i>;
174+
else</ins>
175+
return x.<i>current</i> == ranges::end(x.<i>parent_</i>-&gt;<i>base_</i>) &amp;&amp; !x.<i>trailing_empty_</i>;
176+
</pre></blockquote>
177+
</blockquote>
178+
</blockquote>
179+
180+
</li>
181+
182+
</ol>
183+
</resolution>
184+
185+
</issue>

0 commit comments

Comments
 (0)