Выбрать главу

$ echo "a a a b c foo bar foobar foo bar bar" | ./program

a, b, bar, c, foo, foobar,

Как это работает

В данной программе можно отметить два интересных момента. Первый из них состоит в том, что мы применяем итератор std::istream_iterator, чтобы получить доступ к данным, введенным пользователем. Второй момент: для записи этих данных в наш контейнер std::set мы задействуем алгоритм std::copy, который обернули в экземпляр класса std::inserter! Может показаться удивительным то, что всего одна строка кода выполняет всю работу по токенизации входных данных, помещению их во множество, отсортированное по алфавиту, и отсечению дубликатов.

std::istream_iterator

Этот класс очень интересен в тех случаях, когда мы хотим обрабатывать большие объемы однотипных данных, получаемые из потока, чему и посвящен наш пример: мы анализируем все входные данные слово за словом и помещаем их в множество в виде экземпляров класса std::string.

Итератор std::istream_iterator принимает один шаблонный параметр с необходимым нам типом. Мы выбрали тип std::string, поскольку ожидаем, что будем работать со словами, но это могут быть, например, и числа с плавающей точкой. По сути, здесь годится любой тип, для которого можно записать cin>>var;. Конструктор принимает экземпляр класса istream. Стандартный поток ввода представляется глобальным объектом потока ввода std::cin, который вполне подходит для нашего случая.

istream_iterator<string> it {cin};

К созданному итератору потока ввода применимы две операции. Во-первых, при разыменовании (*it) он возвращает текущий введенный символ. Поскольку мы указали, что итератор соотнесен с типом std::string с помощью шаблонного параметра, этот символ будет содержать одно слово. Во-вторых, при инкременте (++it) итератор переходит на следующее слово, к которому мы также можем получить доступ путем разыменования.

Но погодите, нужно быть аккуратными после выполнения операции инкремента и до разыменования. При пустом стандартном потоке ввода итератор нельзя разыменовывать. Вместо этого следует завершить цикл, в котором мы разыменовываем итератор, для получения каждого слова. Условие остановки, позволяющее узнать, что итератор стал некорректным, — сравнение с конечным итератором. Если сравнение it==end выполнилось, то мы дошли до последнего введенного слова.

Конечный итератор — это экземпляр std::istream_iterator, созданный с помощью стандартного конструктора без параметров. Он нужен для сравнения в условии остановки, проверяемом на каждой итерации цикла:

istream_iterator<string> end;

Как только std::cin окажется пустым, итератор it обнаружит это и наше сравнение с конечным оператором вернет результат true.

std::inserter 

Мы использовали пару итераторов it и end как итераторы для работы с входными данными в вызове std::copy. Третий параметр должен быть итератором для работы с выходными данными. Здесь мы не можем просто взять итератор s.begin() или s.end(). Для пустого множества они будут одинаковыми, так что нам даже нельзя разыменовать их независимо от того, делаем мы это для чтения (из) или присваивания (в).

Тут вступает в дело std::inserter. Это функция, возвращающая итератор std::insert_iterator, который ведет себя как итератор, но при этом делает что-то отличное от того, что делают обычные итераторы. Выполнение операции инкремента для данного итератора ничего не даст. Когда мы его разыменовываем и присваиваем значение, он берет контейнер, прикрепленный к нему, и добавляет в него заданное значение как новый элемент!

При создании экземпляра std::insert_iterator с помощью std::inserter вам понадобятся два параметра:

auto insert_it = inserter(s, s.end());

Здесь s — наше множество, а s.end() — итератор, указывающий на место, куда должен быть вставлен новый элемент. Для пустого множества, с которого и начинается наш пример, этот итератор эквивалентен s.begin(). В других структурах данных наподобие векторов или списков второй параметр критически важен при определении того, куда именно итератор вставки должен добавить новые элементы.

Собираем все воедино

В конце концов все волшебство происходит во время вызова метода std::copy: