Усложняя стандартный пример +13


Стандартная библиотека С++ предлагает не только набор классов, но также определяет способ написания программ. В рамках данной статьи рассматриваются общие требования к реализации программ при помощи STL.

Рассмотрим следующую задачу:

Считать из файла input.txt массив целых чисел, разделенных пробельными символами. Отсортировать их и записать в файл output.txt

Можно написать следующее решение:

#include <vector>
#include <algorithm>
#include <fstream>

int main(){
    // открываем input.txt для чтения
    std::ifstream fin("input.txt");
    // открываем output.txt для записи
    std::ofstream fout("output.txt");
    // объявление и инициализация пустого целочисленного вектора
    std::vector<int> v;

    // сложная магия, благодаря которой из потока чтения вставляются элементы в конец вектора
    std::copy(std::istream_iterator<int>(fin), std::istream_iterator<int>(), std::inserter(v, v.end()));
    // алгоритм сортировки
    std::sort(v.begin(), v.end());
    // сложная магия, благодаря которой элементы из вектора копируются в поток записи
    std::copy(v.begin(), v.end(), std::ostream_iterator<int>(fout, " "));
    return 0;
}

Несколько слов о «магии» в коде:

  • Одной из основ библиотеки являются итераторы, а также полуинтервалы, ими определяемые. По семантике (читай — по поведению) они совпадают с указателями. То есть, опреатор разыменования * вернет вам элемент, на который ссылается итератор, ++ переведет итератор на следующий элемент. В частности, любой контейнер представляется его концевыми итераторами [begin, end), где begin указывает на первый элемент, end — за последний;
  • Алгоритмы, работающие с контейнерами, в качестве параметров принимают начало и конец контейнера (или его части);
  • Алгоритм копирования copy просто переписывает элементы из одного полуинтервала в другой. Если в целевом контейнере не выделена память, то поведение непредсказуемо [copy];
  • Функция inserter вставляет значение в контейнер перед указанным итератором [inserter]
  • istream_iterator и ostream_iterator предоставляют доступ к потокам в стиле контейнеров [istream_iterator, ostream_iterator]

Этот пример, на самом деле, довольно прост. Однако он может нам помочь в решении следующей задачи:
В файле input.txt хранится список, содержащий информацию о людях: фамилия, имя, возраст (каждая строка это запись, данные разделены пробелом). Считать эти данные в массив, отсортировать их по возрасту и записать в файл output.txt. Вывести на экран информацию о человеке, чей возраст более 20, но менее 25 лет.
В принципе, решение будет практически таким же. Однако, для сохранения решения необходимо провести подготовительную работу, а именно:

  1. Объявить структуру данных. — можно определить что-то служное, но в конкретном случае достаточно struct:

    struct man{
        std::string firstname, secondname;
        size_t age;
    };
    

    Настоятельно рекомендую подумать над реализацией конструкторов копирования, с параметрами и по умолчанию, а также оператора копирования. При дальнейшем развитии проекта они вам обязательно понадобятся.
  2. Перегрузить операторы ввода / вывода — этими операторами манипулируют итераторы над потоками. Да и вообще, ими привычнее пользоваться.

    std::ostream& operator << (std::ostream& out, const man& p){
        out << p.firstname << " " << p.secondname << " " << p.age;
        return out;
    }
    
    std::istream& operator >> (std::istream& in, man& p){
        in >> p.firstname >> p.secondname >> p.age;
        return in;
    }
  3. Определить правила упорядочивания объектов — Здесь уже большое раздолье: можно переопределить оператор <, можно описать функцию, функтор или лямбда-выражение. В данном случае воспользуемся функцией.

    bool comparator(const man& p1, const man& p2){
        return p1.age < p2.age;
    }
    
  4. Определить правило выборки объектов — Опять же, довольно большой выбор реализации. На этот раз пусть будет функтор (объект класса, в котором определен оператор круглые скобки [функтор]), которому можно передавать возрастной промежуток:

    struct predicate{
        size_t begin, end;
        predicate(int p1, int p2): begin(p1), end(p2) {}
        bool operator ()(const man& p){
            return (p.age > begin) && (p.age < end);
        }
    };

    Обратите внимание на конструктор у функтора — таким образом мы можем настраивать его поведение.

Ну и, собственно, точка входа в программу:

int main(){
    std::ifstream fin("input.txt");
    std::ofstream fout("output.txt");

    std::vector<man> v;

    std::copy(std::istream_iterator<man>(fin), std::istream_iterator<man>(), std::inserter(v, v.end()));
    std::sort(v.begin(), v.end(), comparator);

    std::copy_if(v.begin(), v.end(), std::ostream_iterator<man>(std::cout, "\n"), predicate(20, 25));

    std::copy(v.begin(), v.end(), std::ostream_iterator<man>(fout, "\n"));
    return 0;
}

Как можно заметить, изменения функции main минимальные, касаются только типа элементов вектора. Плюс добавлен вызов алгоритма copy_if. Этот полезный алгоритм появился со стандартом С++11, он копирует элементы из одного контейнера в дргой только те элементы, которые удовлетворяют условию.

Какие можно сделать из этого выводы?

  1. Знание и активно использование алгоритмов стандартной библиотеки существенно ускоряет разработку (точнее говоря, доводит до автоматизма).
  2. Полезным является объявление различных конструкторов и операторов копирования для структур данных. Они используются в различных ситуациях, в частности, при вставке элементов в контейнеры.
  3. Для удобства можно перегрузить операторы ввода и вывода, а также оператор сравнения и оператор упорядочения.
  4. Функторы — мощный инструмент, который позволяет реализовать функции с «памятью» или дополнительным поведением
  5. … возможно, еще каккие-либо...

Спасибо, что дотерпели!

Весь код программы:

an_example.cpp
#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
#include <iostream>
#include <iterator>

struct man{
    std::string firstname, secondname;
    size_t age;
};

std::ostream& operator << (std::ostream& out, const man& p){
    out << p.firstname << " " << p.secondname << " " << p.age;
    return out;
}

std::istream& operator >> (std::istream& in, man& p){
    in >> p.firstname >> p.secondname >> p.age;
    return in;
}

bool comparator(const man& p1, const man& p2){
    return p1.age < p2.age;
}

struct predicate{
    size_t begin, end;
    predicate(int p1, int p2): begin(p1), end(p2) {}
    bool operator ()(const man& p){
        return (p.age > begin) && (p.age < end);
    }
};

int main(){
    std::ifstream fin("input.txt");
    std::ofstream fout("output.txt");

    std::vector<man> v;
    std::vector<man>::iterator i;

    std::copy(std::istream_iterator<man>(fin), std::istream_iterator<man>(), std::inserter(v, v.end()));
    std::sort(v.begin(), v.end(), comparator);

    std::copy_if(v.begin(), v.end(), std::ostream_iterator<man>(std::cout, "\n"), predicate(20, 25));

    std::copy(v.begin(), v.end(), std::ostream_iterator<man>(fout, "\n"));
    return 0;
}

Библиография:

  1. Stepanov Al. Lee Meng, The Standard Template Library, 1995
  2. CPP Reference, copy
  3. CPP Reference, inserter
  4. CPP Reference, istream_iterator
  5. CPP Reference, ostream_iterator
  6. Wiki, функтор




К сожалению, не доступен сервер mySQL