Range-based loop over an input stream

2020-06-03 05:05发布

问题:

To iterate over an input stream, we would usually use a std::istream_iterator like so:

typedef std::istream_iterator<std::string> input_iterator;

std::ifstream file("myfile");
for (input_iterator i(file); i != input_iterator(); i++) {
  // Here, *i denotes each element extracted from the file
}

It'd be nice if we could use the range-based for statement to iterate over input streams. However, for objects of class type, range-based for requires the object to have begin() and end() member functions (§6.5.4, bold emphasis added):

  • if _RangeT is an array type, begin-expr and end-expr are __range and __range + __bound, respectively, where __bound is the array bound. If _RangeT is an array of unknown size or an array of incomplete type, the program is ill-formed;

  • if _RangeT is a class type, the unqualified-ids begin and end are looked up in the scope of class _RangeT as if by class member access lookup (3.4.5), and if either (or both) finds at least one declaration, begin-expr and end-expr are __range.begin() and __range.end(), respectively;

  • otherwise, begin-expr and end-expr are begin(__range) and end(__range), respectively, where begin and end are looked up with argument-dependent lookup (3.4.2). For the purposes of this name lookup, namespace std is an associated namespace.

The input streams don't have these member functions (they are not Containers) and so range-based for won't work on them. This makes sense anyway because you would need some way to specify the type to extract (std::string in the case above).

But if we know what we want to extract, is it possible to define our own begin() and end() functions (perhaps specializations or overloads of std::begin() and std::end()) for input streams such that they would be found by class member access lookup as described above?

It's unclear (at least to me) from §6.5.4 whether the functions will then be looked up with argument-dependent lookup if the previous lookup fails. Another thing to consider is that std::ios_base and its derivatives already have a member called end which is a flag for seeking.

Here's the intended result:

std::ifstream file("myfile");
for (const std::string& str : file) {
  // Here, str denotes each element extracted from the file
}

Or:

std::ifstream file("myfile");
for (auto i = begin(file); i != end(file); i++) {
  // Here, *i denotes each element extracted from the file
}

回答1:

An obvious approach is to use a simple decorator for your stream providing the type and the necessary interface. Here is how this could look like:

template <typename T>
struct irange
{
    irange(std::istream& in): d_in(in) {}
    std::istream& d_in;
};
template <typename T>
std::istream_iterator<T> begin(irange<T> r) {
    return std::istream_iterator<T>(r.d_in);
}
template <typename T>
std::istream_iterator<T> end(irange<T>) {
    return std::istream_iterator<T>();
}

for (auto const& x: irange<std::string>(std::ifstream("file") >> std::skipws)) {
    ...
}


回答2:

It doesn't matter whether they will be found by argument-dependent lookup, because you are allowed to put specializations of classes and functions in the std namespace.



回答3:

Here is one possible solution. Sadly, it does require an extra structure:

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <string>

struct S {
  std::istream& is;
  typedef std::istream_iterator<std::string> It;
  S(std::istream& is) : is(is) {}
  It begin() { return It(is); }
  It end() { return It(); }
};

int main () {
  std::ifstream file("myfile");
  for(auto& string : S(file)) {
    std::cout << string << "\n";
  }
}

Another solution is to derive from std::ifstream:

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <string>


struct ifstream : std::ifstream {
  // using std::ifstream::ifstream; I wish g++4.7 supported inheriting constructors!
  ifstream(const char* fn) : std::ifstream(fn) {}
  typedef std::istream_iterator<std::string> It;
  It begin() { return It(*this); }
  It end() { return It(); }
};

int main () {
  ifstream file("myfile");
  for(auto& string : file) {
    std::cout << string << "\n";
  }
}


回答4:

I attempted to pursue the idea of specializing std::begin and std::end for classes derived from std::basic_istream (I'm not so great at this template metaprogramming business):

namespace std
{
  template <typename C>
  typename
  std::enable_if<
    std::is_base_of<std::basic_istream<typename C::char_type>, C>::value,
    std::istream_iterator<std::string>>::type
  begin(C& c)
  {
    return {c};
  }

  template <typename C>
  typename
  std::enable_if<
    std::is_base_of<std::basic_istream<typename C::char_type>, C>::value,
    std::istream_iterator<std::string>>::type
  end(C& c)
  {
    return {};
  }
}

Actually, it works pretty well. I didn't create versions that take const C& because I don't think it makes sense to extract from a const stream (and I had errors when I tried to do so). I'm also not sure if I can make this more move friendly. So now I can print out the contents of myfile like so::

std::ifstream file("myfile");
std::copy(begin(file), end(file), std::ostream_iterator<std::string>(std::cout, " "));

So these begin and end functions work as expected. However, it falls flat when used in a range-based for loop. std::basic_istream classes are derived from std::ios_base which already has a member called end (it's a flag for seeking within a stream). Once the range-based for loop finds this, it just gives up because it can't find a corresponding begin (not to mention that end is not the right kind of entity):

main.cpp:35:33: error: range-based ‘for’ expression of type ‘std::basic_ifstream’ has an ‘end’ member but not a ‘begin’

The only alternative that works in both situations, as others have mentioned, is to create a wrapper object. Unfortunately that end member in std::ios_base completely ruins any chance of implementing this in a nice way.