admin管理员组文章数量:1186135
I was banging my head against a wall trying to figure out why this implementation of an operator <<
overload for printing vectors with cout
was causing segfaults if used with empty vectors.
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
out << '[';
for(auto p=v.begin(); p<v.end()-1; ++p) {
out << *p << ", ";
}
if(!v.empty()) {
out << v.back();
}
out << ']';
return out;
}
After some debugging I realized that the loop condition was returning true
for empty vectors so I tried to fix it several ways until landing on this solution with the help of ChatGPT.
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
out << '[';
for(auto p=v.begin(); p+1<v.end(); ++p) {
out << *p << ", ";
}
if(!v.empty()) {
out << v.back();
}
out << ']';
return out;
}
Notice the change in the condition of the loop essentially goes from v.start() < v.end()-1
to v.start()+1 < v.end()
in the problem case of empty vectors. I tested the solution and found that it works fine now. I asked ChatGPT why this works and it basically told me that I should respect the valid range of vector iterators: [v.start()
, v.end()
]. Yet I find it curious that v.start()+1
results in correct behavior since in the case of empty vectors v.begin()==v.end()
thus v.begin()+1
is outside of the valid range thus should be undefined behavior.
I asked again about the difference between accessing the range before v.start()
and the range after v.end()
and the response boiled down to its ok to do the latter but not the former since we are not dereferencing the iterator. But using this logic wouldn't it be fine if we did either? Shouldn't both be undefined behavior? Does this kind of thing apply to other types of containers that support iterators? Is container.end()+1
valid or not for arithmetic and comparison purposes?
I was banging my head against a wall trying to figure out why this implementation of an operator <<
overload for printing vectors with cout
was causing segfaults if used with empty vectors.
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
out << '[';
for(auto p=v.begin(); p<v.end()-1; ++p) {
out << *p << ", ";
}
if(!v.empty()) {
out << v.back();
}
out << ']';
return out;
}
After some debugging I realized that the loop condition was returning true
for empty vectors so I tried to fix it several ways until landing on this solution with the help of ChatGPT.
template<typename T>
ostream& operator << (ostream& out, const vector<T>& v) {
out << '[';
for(auto p=v.begin(); p+1<v.end(); ++p) {
out << *p << ", ";
}
if(!v.empty()) {
out << v.back();
}
out << ']';
return out;
}
Notice the change in the condition of the loop essentially goes from v.start() < v.end()-1
to v.start()+1 < v.end()
in the problem case of empty vectors. I tested the solution and found that it works fine now. I asked ChatGPT why this works and it basically told me that I should respect the valid range of vector iterators: [v.start()
, v.end()
]. Yet I find it curious that v.start()+1
results in correct behavior since in the case of empty vectors v.begin()==v.end()
thus v.begin()+1
is outside of the valid range thus should be undefined behavior.
I asked again about the difference between accessing the range before v.start()
and the range after v.end()
and the response boiled down to its ok to do the latter but not the former since we are not dereferencing the iterator. But using this logic wouldn't it be fine if we did either? Shouldn't both be undefined behavior? Does this kind of thing apply to other types of containers that support iterators? Is container.end()+1
valid or not for arithmetic and comparison purposes?
4 Answers
Reset to default 5Both approaches can work if the vector is tested first for empty.
Otherwise, you are risking undefined behavior adding/subtracting the iterator values. Outputting the first element before the loop seems more readable. Inside the loop, the comma precedes the value's output.
If you can use the ranges library, the range-for is usable similarly using drop
to hide the first element for the loop.
Here is a simplified demonstration and testing code:
auto main() -> int {
std::vector vec{1, 2, 3};
#if 0
vec.clear();
#endif
if (!vec.empty()) {
std::cout << vec[0];
for (auto v = vec.begin(); v != vec.end() - 1; v++) {
std::cout << ", " << *v;
}
std::cout << '\n';
}
if (!vec.empty()) {
std::cout << vec[0];
for (auto v = vec.begin()+1; v != vec.end(); v++) {
std::cout << ", " << *v;
}
std::cout << '\n';
}
// If you can use ranges
if (!vec.empty()) {
std::cout << vec[0];
for (auto v : vec | drop(1)) {
std::cout << ", " << v;
}
std::cout << '\n';
}
}
Update 1 & 2: I couldn't resist trying the latest std::print
in C++23. Added simple std::println
in response to comment. I didn't try it with CLang in Compiler Explorer because it wouldn't work on my system with GCC. Duh!
Live code:
if (!vec.empty()) {
std::print("{}", vec[0]);
auto drop_cnt = (vec.size() > 1) ? 1 : 0;
std::println(", {:n}", vec | vws::drop(drop_cnt));
}
println("{:n}", vec ); // :n removes the surrounding []
Shouldn't both be undefined behavior?
Yes, both are undefined behavior. The strange thing about undefined behavior is that anything can happen. Anything. The program could crash. It could erase your hard drive. It could appear to work. Anything.
The fact that you got the result you wanted does not conclusively rule out undefined behavior.
Is container.end()+1 valid or not for arithmetic and comparison purposes?
It is not valid.
Possibly where ChatGPT got confused is that container.end()
is safe to use as long as you do not de-reference it. This is close to what it told you, but not quite. And to some extent, that is what generative AI is designed to do – take language as input, find "close" connections, and generate, allowing minor variations in the details. This is helpful in that the same input can produce different results each time it is used. On the other hand "close" is not so helpful when details matter, as they do in programming.
With the current state of AI, you can leverage AI to get pointed in the right direction, but don't expect the AI to get the details correct. If something from an AI seems to not make sense, there is a good chance that it does not make sense.
You're indeed running into what's technically speaking undefined behaviour - or rather, determined by the implementation of this iterator, not by its interface(+documentation). Since implementation could change under the hood in any way, you as a user shouldn't rely on it. This applies to anything, even if you don't dereference. Just gotta follow the usual programming rules - check for validity before any operations that could be invalid - and you're safe.
Based on your results, we can even guess at what the implementation of iterator is. It can hold an unsigned integer with 0 corresponding to begin()
and size()
corresponding to end()
. In that case, incrementing it and comparing with size()
gives correct results, barring overflow. However, decrementing 0 causes underflow and comparing the integer stored in the valid pointer p
with maximum unsigned integer always gives true
.
Note that this is similar to using plain indexes: for(int i = 0; i < v.size()-1; i++)
is an infinite loop for empty v
. Same if i
is unsigned since it's converted to unsigned for comparison. This is a famous bug, although not the same kind since it's defined by the standard and return type of size()
.
Yes, v.begin() + 1
and v.end() - 1
both give undefined behaviour if v
is an empty container (i.e. if v.begin() == v.end()
).
Personally, I'd strongly recommend against defining such an operator<<()
in the first place. If you must have such a function, give it a different (distinct) name.
There are numerous explanations/interpretations online of why the standard does not specify such an operator function for standard containers (not least of which is that it allows containers to have elements that lack an operator<<()
) and explanations of why it is a bad idea to roll your own.
But - if you have the bit in your teeth and really insist on having such an operator despite such advice - a simpler fix for your code would be (also removing reliance on using namespace std
or other using
s that have similar net effect)
template<typename T>
std::ostream& operator << (std::ostream& out, const std::vector<T>& v)
{
out << '[';
auto end = p.end();
for(auto p = p.begin(); p != end; ++p)
{
out << *p;
if (p + 1 != end) // ok since p != end
out << ", ";
}
out << ']';
return out;
}
本文标签: ccontainerbegin()1 validity for empty container vs containerend()1Stack Overflow
版权声明:本文标题:c++ - container.begin()+1 validity for empty container vs container.end()-1 - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1738295016a2073339.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
p<v.end()
, and print the comma before the item. Others prefer looping the whole container and using anif
statement in the to print the comma if not at the end. – user4581301 Commented Jan 26 at 21:26operator<<
is fine for user-defined types but your overload accepts anyT
s. – Ted Lyngmo Commented Jan 26 at 21:44begin()
iterator to output a comma. – PaulMcKenzie Commented Jan 26 at 23:07