How to safely read a line from an std::istream?
Solution 1:
You could write your own version of std::getline
with a maximum number of characters read parameter, something called getline_n
or something.
#include <string>
#include <iostream>
template<typename CharT, typename Traits, typename Alloc>
auto getline_n(std::basic_istream<CharT, Traits>& in, std::basic_string<CharT, Traits, Alloc>& str, std::streamsize n) -> decltype(in) {
std::ios_base::iostate state = std::ios_base::goodbit;
bool extracted = false;
const typename std::basic_istream<CharT, Traits>::sentry s(in, true);
if(s) {
try {
str.erase();
typename Traits::int_type ch = in.rdbuf()->sgetc();
for(; ; ch = in.rdbuf()->snextc()) {
if(Traits::eq_int_type(ch, Traits::eof())) {
// eof spotted, quit
state |= std::ios_base::eofbit;
break;
}
else if(str.size() == n) {
// maximum number of characters met, quit
extracted = true;
in.rdbuf()->sbumpc();
break;
}
else if(str.max_size() <= str.size()) {
// string too big
state |= std::ios_base::failbit;
break;
}
else {
// character valid
str += Traits::to_char_type(ch);
extracted = true;
}
}
}
catch(...) {
in.setstate(std::ios_base::badbit);
}
}
if(!extracted) {
state |= std::ios_base::failbit;
}
in.setstate(state);
return in;
}
int main() {
std::string s;
getline_n(std::cin, s, 10); // maximum of 10 characters
std::cout << s << '\n';
}
Might be overkill though.
Solution 2:
There is already such a getline
function as a member function of istream
, you just need to wrap it for buffer management.
#include <assert.h>
#include <istream>
#include <stddef.h> // ptrdiff_t
#include <string> // std::string, std::char_traits
typedef ptrdiff_t Size;
namespace my {
using std::istream;
using std::string;
using std::char_traits;
istream& getline(
istream& stream, string& s, Size const buf_size, char const delimiter = '\n'
)
{
s.resize( buf_size ); assert( s.size() > 1 );
stream.getline( &s[0], buf_size, delimiter );
if( !stream.fail() )
{
Size const n = char_traits<char>::length( &s[0] );
s.resize( n ); // Downsizing.
}
return stream;
}
} // namespace my
Solution 3:
Replace std::getline by creating a wrapper around std::istream::getline:
std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
{
try
{
str.resize(n);
is.getline(&str[0],n,delim);
str.resize(is.gcount());
return is;
}
catch(...) { str.resize(0); throw; }
}
If you want to avoid excessive temporary memory allocations, you could use a loop which grows the allocation as needed (probably doubling in size on each pass). Don't forget that exceptions might or might not be enabled on the istream object.
Here's a version with the more efficient allocation strategy:
std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
{
std::streamsize base=0;
do {
try
{
is.clear();
std::streamsize chunk=std::min(n-base,std::max(static_cast<std::streamsize>(2),base));
if ( chunk == 0 ) break;
str.resize(base+chunk);
is.getline(&str[base],chunk,delim);
}
catch( std::ios_base::failure ) { if ( !is.gcount () ) str.resize(0), throw; }
base += is.gcount();
} while ( is.fail() && is.gcount() );
str.resize(base);
return is;
}