I know that modifying the target of filter_view::iterator
is allowed, but if the resulting value does no longer satisfy the filter predicate this results in undefined behaviour.
What about the following code that modifies the target of take_while_view::iterator
?
#include <iostream>
#include <ranges>
#include <string>
#include <vector>
struct Token
{
std::string value;
bool used = false;
};
int main()
{
auto tokens = std::vector{ Token{"p1"}, Token{"p2"}, Token{"++"}, Token{"p3"} };
auto view = tokens
| std::views::drop_while([](auto const& token) { return token.used; })
| std::views::take_while([](auto const& token) { return !token.used; });
auto transform = view
| std::views::transform([](auto& token)
{
token.used = true;
return token.value;
})
| std::views::common;
auto strs = std::vector(transform.begin(), transform.end());
for (auto const& str : strs)
{
std::cout << str << ", ";
}
return 0;
}
The code is supposed to skip tokens at the start that have already been processed (the drop_while
part) and then process the group of tokens until the next processed one is found (the take_while
part).
The above compiles fine under gcc, clang, and msvc. Code generated by gcc amd clang seems to work fine. Clang's sanitizers see no issue, as does Valgrind. However, when I run under msvc's debugger, creating the vector
from transform
triggers an error:
"Expression: Cannot increment transform_view iterator past end".
I think I trigger the error by modifying the range that take_while
allows, however I haven't found anything that would disallow this (like for filter
).
Is my code incorrect? How do I fix it? Are there any rules of what is and is not allowed in composed views?
Your transform
is broken:
| std::views::transform([](auto& token)
{
token.used = true;
return token.value;
})
The projection passed to transform
is required to be regular_invocable
. Which means, from [concept.regularinvocable]:
The invoke function call expression shall be equality-preserving ([concepts.equality]) and shall not modify the function object or the arguments.
The mutation here is idempotent, but it is still a mutation, and we're not allowed to modify the arguments at all. Note that this also makes your range not really a forward range, since the second iteration it becomes empty:
fmt::print("{}\n", transform); // ["p1", "p2", "++", "p3"]
fmt::print("{}\n", transform); // []
This happens to work out fine in some implementations because constructing a vector
out of transform
does two passes — but the first pass does not have to read any data, it's just computing the distance. So we avoid any calls to operator*
that mutate the range.
But MSVC catches this because it adds this assertion:
constexpr _Iterator& operator++() noexcept(noexcept(++_Current)) /* strengthened */ {
#if _ITERATOR_DEBUG_LEVEL != 0
_STL_VERIFY(_Parent != nullptr, "Cannot increment value-initialized transform_view iterator");
_STL_VERIFY(
_Current != _RANGES end(_Parent->_Range), "Cannot increment transform_view iterator past end");
#endif // _ITERATOR_DEBUG_LEVEL != 0
++_Current;
return *this;
}
While you're constructing the vector
, that will dereference an iterator — setting token.used
to true
. Then we increment that iterator, which will compare it to end()
. But in that increment, it already compares equal to end()
— because it == end()
in this case will check to see if token.used
is true
(as part of the take_while
). Hence, the assertion.
In the non-debug case, we can get away with this because we're not at the "real" end yet.
The original problem is the transform
. You're not allowed to do mutation like that.