Stream iterators
Pisałem o numeric_limits jako zapomnianej części biblioteki numerycznej, dziś do tego „worka” mogę dorzucić iteratory strumieni jako rzadko używane „twory”. Niektórych może zdziwić ten fakt, że strumienie także posiadają iteratory, a ich wykorzystanie może bardzo nam pomoc i uprościć kod.
Na początek najlepiej zapoznać się z tym co mówi dokumentacja na temat istream_iterator i ostream_iterator z nagłówka iterator.
Często iteratory te są wykorzystywane w różnych przykładach innych algorytmów do szybkiego pobierania danych z strumienia wejściowego i wyświetlania na wyjściowym, w produkcyjnym oprogramowaniu rzadko je można spotkać.
Nasz prosty programik będzie pobierał dane z strumienia wejściowego i umieszczał je w kontenerze, a następnie na tych danych będziemy wykonywać jakieś operacje, po czym na koniec „wypluwać” wynik na wyjściowy strumień.
Z pomocą iteratorów strumieni i algorytmów biblioteki standardowej, łatwo możemy zaimplementować obsługę strumieni – pobieranie/wysyłanie danych z/do wektora. W naszym przypadku moglibyśmy wykorzystać algorytm std::copy (z nagłówka algorithm) i funkcje te przybrałyby bardzo prostą postać:
inline void get_data(std::istream& from, std::vector<std::string>& to) { std::copy(std::istream_iterator<std::string>(from), std::istream_iterator<std::string>(), std::back_inserter(to)); } inline void put_data(std::vector<std::string>& from, std::ostream& to) { std::copy(from.begin(), from.end(), std::ostream_iterator<std::string>(to, "\n")); } |
Nasz programik sortujący wprowadzone słowa:
int main() { std::vector<std::string> vec; get_data(std::cin, vec); std::sort(vec.begin(), vec.end()); std::cout << std::endl << "Sorted:" << std::endl; put_data(vec, std::cout); return 0; } |
Nic nie stoi na przeszkodzie, aby podobnie operować na strumieniach plikowych – fstream.
Warto jeszcze wspomnieć, że istnieją podobne iteraotry dla buforów strumieniowych – istreambuf_iterator i ostreambuf_iterator.
Bardzo przydatne może się okazać narzędzie służące do szybkiego zapisywania do strumienia wyjściowym zawartości kontenera, tak jak chociażby w wyżej zdefiniowanej funkcji put_data.
Małą niedogodnością jest tutaj wypisywanie niepotrzebnego na końcu delimitera, ale można się tego łatwo pozbyć.
Poniżej prosta funkcja do „wypluwania” zawartości dowolnego kontenera.
template<typename ContainerType> inline void put_data(std::ostream& os, ContainerType& con, const std::string& delim) { typename ContainerType::const_iterator last = con.end(); std::advance(last, -1); if (con.size() > 1) std::copy(con.begin(), last, std::ostream_iterator<typename ContainerType::value_type>(os, delim.c_str())); if (con.size() > 0) os << *last; } |
Funkcja działa bezproblemowo dla kontenerów przechowujących proste typy, a dokładniej takie, które posiadają zdefiniowany operator << dla strumienia wyjściowego z naszym typem, dla pozostałych typów, zdefiniowanych przez użytkownika, musimy zadbać o takowy operator.
Podobnie, aby funkcja współdziałała z kontenerów przechowujących pary powiązanych ze sobą kluczy i wartości, tak jak std::map niezbędne jest dodanie dodatkowego mechanizmu do prezentacji takich danych, najlepiej chyba w postaci:
klucz - wartość |
Mechanizmem tym jest wspomniany operator << dla danej pary:
template<class T, class U> std::ostream& operator<<(std::ostream& os, const std::pair<T, U>& p) { return os << p.first << " - " << p.second; } |
Dla poprawnego działania, operator ten powinien zostać zdefiniowany w przestrzeni nazw std.
I na koniec prosty programik prezentujący i testujący działanie naszego mechanizmu.
Dla prostoty zdefiniujmy sobie dodatkowe operatory << strumienia wyjściowego z używanymi kontenerami:
std::ostream& operator<<(std::ostream& os, const std::vector<int>& vec) { put_data(os, vec, ", "); return os; } std::ostream& operator<<(std::ostream& os, const std::map<int, int>& vec) { put_data(os, vec, ", "); return os; } |
A program przyjmie prostą postać:
int main() { std::vector<int> vec(10); int array[] = { 1, 2, 3, 4, 5 }; vec.assign(array, array + sizeof(array) / sizeof(int)); std::cout << "vector sized at " << vec.size() << ": " << vec << std::endl; vec.resize(1); std::cout << "vector sized at " << vec.size() << ": " << vec << std::endl; vec.clear(); std::cout << "vector sized at " << vec.size() << ": " << vec << std::endl; std::map<int, int> map; map[0] = 1; map[1] = 2; map[2] = 3; std::cout << "map sized at " << map.size() << ": " << map << std::endl; return 0; } |
Myślę, że łatwo można zauważyć to, że iteratory strumieni, w niektórych sytuacjach mogą być bardzo pomocne i nieraz mogą ułatwić nam pracę.