I have recently learned that one can opt-in into exceptions for iostreams. In order to not have to check manually if a file is open I tried that and ran into this behaviour:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <sstream>
#include <vector>
void test(std::istream& is, bool exceptions) {
try {
if (exceptions)
is.exceptions(std::istream::failbit);
std::vector<int> input;
std::copy(std::istream_iterator<int>{is}, {}, std::back_inserter(input));
for (auto x : input) {
std::cout << x << '\n';
}
}
catch (const std::ios_base::failure& f) {
std::cerr << "Caught error: " << f.what() << '\n';
}
}
int main() {
// Emulates file
std::stringstream ss("1 2 3\n4 5 6\n7 8 9\n");
test(ss, true);
}
When exceptions are set off this works normally. But when I use exceptions I get one thrown from basic_ios::clear
and I can't think of a reason why.
basic_ios::clear
is not listed under the function that can set the failbit
according to cppreference.
Thanks in advance.
Edit: The answers below already answer why this happens. My additional question now is how to avoid this exception? My second attempt was to replace std::copy
by this loop:
for (int n; is >> n;) {
input.push_back(n);
}
produced the same exception. Or is this behavior even intended?
Note: clang doesn't show this behavior.
Jonesinator has given you the reason of the exception, I would like just to stress that the error is there independently from the exception or not. In fact your function is not equivalent, you do not check the stream after the operation in the no-exception branch. In fact, an error silently happen. If you write the two functions in an equivalent way you will get the an equivalent result:
#include <iostream>
#include <iterator>
#include <sstream>
#include <vector>
void test_exception(std::istream& is) {
try {
is.exceptions(std::istream::failbit);
std::vector<int> input;
std::copy(std::istream_iterator<int>{is}, {}, std::back_inserter(input));
for (auto x : input) {
std::cout << x << '\n';
}
}
catch (const std::ios_base::failure& f) {
std::cout << "Caught error: " << f.what() << '\n';
}
}
void test_error_code(std::istream& is) {
std::vector<int> input;
std::copy(std::istream_iterator<int>{is}, {}, std::back_inserter(input));
if (!is.good()) {
std::cout << "Caught error!" << std::endl;
return;
}
for (auto x : input) {
std::cout << x << '\n';
}
}
int main() {
// Emulates file
std::stringstream ss_error_code("1 2 3\n4 5 6\n7 8 9\n");
test_error_code(ss_error_code);
std::stringstream ss_exception("1 2 3\n4 5 6\n7 8 9\n");
test_exception(ss_exception);
}
output:
Caught error!
Caught error: basic_ios::clear
IMHO, this is a great example why exceptions are superior over result code in the vast majority of the scenario and they should be used as default.
Using GDB you can see the error happens when the std::istream_iterator
is incremented.
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007ffff71d13fa in __GI_abort () at abort.c:89
#2 0x00007ffff7ae80ad in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3 0x00007ffff7ae6066 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007ffff7ae60b1 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007ffff7ae62c9 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x00007ffff7b0eea3 in std::__throw_ios_failure(char const*) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x00007ffff7b4a82d in std::basic_ios<char, std::char_traits<char> >::clear(std::_Ios_Iostate) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#8 0x00007ffff7b4d52f in std::istream::operator>>(int&) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#9 0x00005555555556c2 in std::istream_iterator<int, char, std::char_traits<char>, long>::_M_read (this=0x7fffffffe230) at /usr/include/c++/6/bits/stream_iterator.h:121
#10 0x0000555555555ac2 in std::istream_iterator<int, char, std::char_traits<char>, long>::operator++ (this=0x7fffffffe230) at /usr/include/c++/6/bits/stream_iterator.h:95
#11 0x0000555555555a36 in std::__copy_move<false, false, std::input_iterator_tag>::__copy_m<std::istream_iterator<int, char, std::char_traits<char>, long>, std::back_insert_iterator<std::vector<int, std::allocator<int> > > > (__first=..., __last=..., __result=...) at /usr/include/c++/6/bits/stl_algobase.h:293
#12 0x0000555555555965 in std::__copy_move_a<false, std::istream_iterator<int, char, std::char_traits<char>, long>, std::back_insert_iterator<std::vector<int, std::allocator<int> > > > (__first=..., __last=..., __result=...) at /usr/include/c++/6/bits/stl_algobase.h:386
#13 0x00005555555557e2 in std::__copy_move_a2<false, std::istream_iterator<int, char, std::char_traits<char>, long>, std::back_insert_iterator<std::vector<int, std::allocator<int> > > > (__first=..., __last=..., __result=...) at /usr/include/c++/6/bits/stl_algobase.h:424
#14 0x00005555555554c9 in std::copy<std::istream_iterator<int, char, std::char_traits<char>, long>, std::back_insert_iterator<std::vector<int, std::allocator<int> > > > (__first=..., __last=..., __result=...) at /usr/include/c++/6/bits/stl_algobase.h:456
#15 0x00005555555550ed in test (is=..., exceptions=true) at sample.cpp:12
#16 0x000055555555521c in main () at sample.cpp:25
Unrolling the loop, you can find that it is the last call to increment that causes the problem, i.e. calling std::istream_iterator::operator++
when the input stream is empty.
Looking closer at the stack trace, the final increment is trying std::istream::operator>> when the stream is empty. According to cppreference, that will result in the failbit being set since the operation failed to extract an integer from the stream.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With