Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП_ Лекция №07 - Стандартная библиотека шаблонов ч.1 .docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
296.29 Кб
Скачать

При работе с другими контейнерами также следует соблюдать осторожность, и задумываться о ситуациях, при которых могут появляться недействительные итераторы:

  • связные списки std::list и std::forward_list наименее подвержены проблеме недействительных итераторов из всех контейнеров, благодаря сильному распределению расположения узлов в динамической памяти - недействительными могут оказаться только те итераторы, которые ссылаются на удаленные элементы;

  • контейнер std::deque делает недействительными итераторы при любой операции вставки или удаления элементов: в 100% случаев итератор после модификации дека более не указывает на логически правильную позицию, а в случае изменений в структуре блоков возможны и некорректные обращения к освобожденной памяти;

  • обертка над массивом std::array никогда не делает итераторы недействительными, поскольку не содержит операций по вставке и удалению элементов.

Алгоритмы стандартной библиотеки

Когда родительский объект предоставляет для внешнего кода доступ к хранимым дочерним объектам через итераторы стандартных контейнеров, появляется дополнительная привлекательная возможность - применение алгоритмов стандартной библиотеки. Все такие алгоритмы принимают набор данных для обработки в виде пары итераторов, задающих начальный элемент и элемент, следующий за последним: [ first; last ). Алгоритмы можно применять к любым структурам данных, предоставляющим итераторы подходящего типа.

Если посмотреть на картину в целом, стандартная библиотека С++ объединяет в единую идеологию взаимодействие трех основных компонентов:

  • контейнеры;

  • итераторы;

  • алгоритмы.

Контейнеры представляют собой шаблонные реализации классических структур данных. Основная задача контейнеров - хранить накопленные данные и предоставлять к ним доступ. Благодаря согласованности имен большинства методов, различные контейнеры являются в значительной мере взаимозаменяемыми. Операции контейнеров, однако, могут характеризоваться различной вычислительной сложностью.

Роль итераторов состоит в сокрытии деталей реализации конкретного контейнера при работе с набором данных. Итераторы различных категорий позволяют просматривать, модифицировать существующие наборы данных независимо от конкретного способа их хранения. Итераторы надежно изолируют код, использующий данные, от кода, отвечающего за организацию их хранения. Это позволяет модифицировать обработку и хранение независимо друг от друга, особенно если код обработки рассчитан на минимальные требования к принимаемым итераторам.

Алгоритмы используют для обработки наборов данных типичными способами, наиболее часто встречающимися в различных программах. Нет никакой необходимости в реализации собственных алгоритмов сортировки, линейного и бинарного поиска, подсчета суммы элементов, теоретико-множественных операций и др. Большинство рутинных задач по обработке данных уже представлены в стандартной библиотеке С++ в виде обобщенных алгоритмов. Библиотека предусматривает около 85 готовых к использованию часто применяемых в различных программах базовых алгоритмов, и постоянно пополняется по мере принятия новых стандартов языка. Алгоритмы используют как непосредственно, так и в качестве строительных блоков для создания более комплексных собственных алгоритмов на основе базовых.

Большинство алгоритмов стандартной библиотеки определяется в заголовочном файле <algorithm>, за исключением численных методов, определенных в файле <numeric>. Для использования стандартных алгоритмов необходимо подключить данные файлы к программе.

В общем виде, типичный алгоритм стандартной библиотеки принимает один или несколько входных наборов данных в виде интервала входных итераторов [first;last), и формирует результирующий набор данных через выходной итератор:

Алгоритмы стандартной библиотеки имеют разные требования к категориям принимаемых ими итераторов. Для любого алгоритма категория итератора всегда оговорена в документации. Также это это несложно понять по автоматическим подсказкам в среде разработки. В частности, в примере, приведенном ниже, очевидно, что стандартный алгоритм сортировки std::sort требует пару итераторов произвольного доступа (_RanIt = Random Access Iterator ):

Отсюда вытекает, что такой алгоритм можно применить далеко не ко всем контейнерам. В частности, не удастся вызвать стандартный алгоритм sort() для связных списков, обладающих менее мощной категорией итераторов. Соответственно, сортировка списков производится по другому, при помощи предусмотренных внутренних методов.

Помимо функциональных возможностей, с каждым стандартным алгоритмом также связывается оценка максимальной вычислительной сложности, которую реализация должна гарантировать. Например, алгоритм binary_search для бинарного поиска в упорядоченной последовательности должен быть реализован с вычислительной сложностью не хуже O(log2N), а алгоритм сортировки - не хуже O(N*log2N). Следует отметить, что гарантии производительности выдаются основываясь на предположении, что вычислительная сложность любой операции переданных входных и выходных итераторов является константной.

В целом, особенности использования конкретных алгоритмов следует изучать по справочной документации по мере непосредственного столкновения. Некоторые алгоритмы применяются чаще других, но в целом, нет никакого смысла запоминать на память требования всех алгоритмов, учитывая наличие большого количества книг и онлайн-справочников, а также удобных средств подсказки в современных средах разработки. Важно понять лишь принципы организации библиотеки.

Ниже приведен простейший пример с использованием стандартных алгоритмов copy (копирование входной последовательности в выходную) и find (линейный поиск во входной последовательности), в котором в начале при помощи алгоритма copy, входных итераторов потока (std::istream_iterator) и выходного итератора вставки в контейнер (std::back_inserter) целочисленные данные с клавиатуры заносятся в вектор. Затем, данные обрабатываются алгоритмом find, возвращающим итератор на возможное вхождение в считанный вектор указанного числа 17:

#include <vector>

#include <iostream>

#include <iterator>

#include <algorithm>

int main ()

{

// Контейнер, хранящий данные

std::vector< int > v;

// Копируем входные данные (из стандартного потока ) в конец вектора

std::copy(

std::istream_iterator< int >( std::cin ),

std::istream_iterator< int >(),

std::back_inserter( v )

);

// Ищем позицию конкретного числа в считанном наборе данных

auto it = std::find( v.begin(), v.end(), 17 );

if ( it != v.end() )

// Позиция обнаружена. // Используем разницу между итераторами для выяснения номера

std::cout << "Value 17 was met at position : " << ( it - v.begin() ) << std::endl;

else

// Позиция не обнаружена

std::cout << "Value 17 was not found" << std::endl;

}

Безусловно, для такой выбранной простой задачи подобный стиль программирования создает чрезмерное обобщение. В то же время, код на основе стандартных алгоритмов обладает удивительной гибкостью. Практически всегда можно заменить контейнер на другой в случае необходимости, при этом не затронув весь остальной использующий его код. Абсолютно всегда можно изменить источник данных, откуда они поступают на обработку, предоставив другую пару итераторов first/last вместо потоковых итераторов.

Если рассмотреть реализацию стандартных алгоритмов внимательно, становится очевидным, что с точки зрения производительности по сравнению с обычными циклами ничего не теряется. Все алгоритмы являются шаблонами функций, итераторы и контейнеры также являются шаблонами. Это означает, что значительная часть инкапсулируемого шаблонами кода разворачивается до простейших инструкций прямого доступа к реализации структур данных при инстанцировании всей цепочки шаблонов. В то же время, наблюдается явный выигрыш с точки зрения абстрагирования от выбора конкретной структуры данных либо источника их поступления.

Алгоритмы стандартной библиотеки условно разделяются на следующие логические группы в зависимости от функционального назначения:

  • немодифицирующие алгоритмы (non-modifying algorithms);

  • модифицирующие алгоритмы (modifying algorithms);

  • разделяющие алгоритмы (partition algorithms);

  • алгоритмы сортировки (sorting algorithms);

  • алгоритмы бинарного поиска (binary search algorithms);

  • алгоритмы обработки отсортированных интервалов (sorted range algorithms);

  • алгоритмы работы с двоичными кучами (heap algorithms);

  • алгортимы поиска минимума-максимума (min-max aglorithms);

  • численные алгоритмы (numeric algorithms).

Немодифицирующие алгоритмы обрабатывают поступающие входные интервалы данных в виде пар итераторов [first;last), при этом не модифицируя ни данные, ни их последовательность. В ходе работы алгоритма вычисляется некоторый интересующий результат - в виде логического значения, целочисленного значения либо позиции в интервале, возвращаемой в виде итератора. Типично, такие алгоритмы ожидают получения самых простых итераторов - входной категории. Ниже указан перечень немодифицирующих алгоритмов вместе с версией стандарта языка, в которой алгоритм был введен, а также гарантированной вычислительной сложностью:

adjacent_find

С++ 98

O(N)

Поиск идентичных идущих в наборе подряд значений

all_of

С++ 11

O(N)

Проверка выполнения указанного условия для всех элементов последовательности

any_of

С++ 11

O(N)

Проверка выполнения указанного условия для хотя бы 1 элемента последовательности

count

С++ 98

O(N)

Подсчет количества значений в последовательности, равных указанному значению

count_if

С++ 98

O(N)

Подсчет количества значений в последовательности, удовлетворяющих указанному условию

equal

С++ 98

O(N)

Проверка эквивалентности двух последовательностей

find

С++ 98

O(N)

Поиск первого вхождения интересующего значения в последовательности

find_end

С++ 98

O(N2)

Поиск последнего вхождения подпоследовательности в другую последовательность

for_each

С++ 98

O(N)

Применение указанной функции ко всем элементам без модификации значений

find_if

С++ 98

O(N)

Поиск первого значения в последовательности, которое удовлетворяет указанному условию

find_if_not

С++ 11

O(N)

Поиск первого значения в последовательности, которое не удовлетворяет критерию

mismatch

С++ 98

O(N)

Поиск первого несоответствия в последовательностях

none_of

С++ 11

O(N)

Проверка невыполнения указанного условия ни на одном элементе последовательности

search

С++ 98

O(N2)

Поиск первого вхождения подпоследовательности в другую последовательность

search_n

С++ 98

O(N2)

Поиск вхождения N повторяющихся значений, идущих в последовательности подряд

Модифицирующие алгоритмы изменяют либо значения хранящихся элементов, либо влияют на их количество или порядок. К модифицирующим алгоритмам относятся:

copy

С++ 98

O(N)

Копирует входную последовательность в выходную

copy_if

C++ 11

O(N)

Копирует элементы входной последовательности в выходную при условии удовлетворения заданному условию-фильтру

copy_n

C++ 11

O(N)

Копирует первые N-элементов входной поданной последовательности в выходную

copy_backward

С++ 98

O(N)

Копирует элементы входной последовательности в выходную начиная с последнего элемента и формируя результат с конца выходной последовательности

fill

С++ 98

O(N)

Заполняет выходную последовательность заданным константным значением

fill_n

С++ 98

O(N)

Заполняет N-значений выходной последовательности заданным константным значением

for_each

С++ 98

O(N)

Применение указанной функции ко всем элементам с модификацией значений

generate

С++ 98

O(N)

Заполняет выходную последовательность данными передаваемого генератора

generate_n

С++ 98

O(N)

Заполняет N-значений выходной последовательности данными передаваемого генератора

move

C++ 11

O(N)

Перемещает значения входной последовательности в выходную

move_backward

C++ 11

O(N)

Перемещает элементы входной последовательности в выходную начиная с последнего элемента и формируя результат с конца выходной последовательности

random_shuffle

С++ 98

O(N)

Случайным образом перемешивает порядок элементов

remove

С++ 98

O(N)

Удаляет элементы с интересующим значением из входной последовательности методом замещения следующими значениями

remove_copy

С++ 98

O(N)

Формирует выходную последовательность из входной, удаляя при переносе элементы, равные интересующему значению

remove_copy_if

С++ 98

O(N)

Формирует выходную последовательность из входной, удаляя при копировании элементы, удовлетворяющие указанному критерию

remove_if

С++ 98

O(N)

Удаляет элементы, удовлетворяющие указанному условию, из входной последовательности методом замещения следующими значениями

replace

С++ 98

O(N)

Заменяет во входной последовательности указанное значение на другое

replace_copy

С++ 98

O(N)

Формирует выходную последовательность как копию входной, при этом заменяя элементы с указанным значением на другие

replace_copy_if

С++ 98

O(N)

Формирует выходную последовательность как копию входной, при этом заменяя элементы, удовлетворяющие указанному условию, на другие

replace_if

С++ 98

O(N)

Заменяет значения во входной последовательности на другое, если они удовлетворяют указанному условию

reverse

С++ 98

O(N)

Переворачивает порядок элементов последовательности на обратный

reverse_copy

С++ 98

O(N)

Формирует выходную последовательность из элементов входной последовательности в обратном порядке

rotate

С++ 98

O(N)

Осуществляет циклический сдвиг элементов последовательности относительно заданной позиции

rotate_copy

С++ 98

O(N)

Формирует выходную последовательность из элементов входной последовательности с учетом циклического сдвига относительно заданной позиции

shuffle

C++ 11

O(N)

Аналогично random_shuffle, однако используется переданный пользователем генератор случайных чисел

swap_ranges

С++ 98

O(N)

Осуществляет обмен значениями между двумя переданными интервалами

transform

С++ 98

O(N)

Применяет указанную функцию преобразования к элементам одной или двух последовательностией (унарная и бинарная форма), записывает результат в выходную последовательность

unique

С++ 98

O(N)

Удаляет одинаковые идущие подряд элементы последовательности методом замещения

unique_copy

С++ 98

O(N)

Формирует выходную последовательность из входной, удаляя при копировании одинаковые идущие подряд элементы

К алгоритмам разделения относятся следующие:

is_partitioned

C++ 11

O(N)

Проверяет является ли последовательность упорядоченной относительно заданного условия разделения (элементы, отвечающие условию идут раньше элементов нарушающих условие)

partition

С++ 98

O(N)

Переупорядочивает элементы последовательности таким образом, чтобы элементы удовлетворяющие указанному критерию стояли левее, а не удовлетворяющие - правее

partition_copy

C++ 11

O(N)

Формирует выходную последовательность таким образом, что в начале будут помещены элементы отвечающие указанному критерию, а в конце - не отвечающие данному критерию

partition_point

C++11

O(N)

Возвращает итератор на позицию разделения в упорядоченной относительно заданного условия разделения

stable_partition

С++ 98

O(N*log2N)

Аналогичен partition, однако переупорядочивает элементы таким образом, чтобы сохранить исходное относительное положение перемещенных элементов

Алгоритмы сортировки отвечают за упорядочивание элементов последовательности:

is_sorted

C++ 11

O(N)

Проверяет является ли интервал отсортированным

is_sorted_until

C++ 11

O(N)

Возвращает итератор на позицию первого элемента, нарушающего порядок сортировки

nth_element

С++ 98

O(N)

Переупорядочивает последовательность таким образом, что все значения, которые меньше n-ного элемента, оказываются перед ним, а все большие значения - после

partial_sort

С++ 98

O(N*log2N)

Переупорядочивает входную последовательность таким образом, что все элементы, стоящие до элемента в указанной позиции будут меньше его и упорядочены по возрастанию, а остальные элементы останутся в произвольном порядке

partial_sort_copy

С++ 98

O(N*log2N)

Аналогично partial_sort, однако формирует выходную последовательность, не модифицируя оригинальную

sort

С++ 98

O(N*log2N)

Сортирует входную последовательность по возрастанию (обычно - методом быстрой сортировки)

stable_sort

С++ 98

O(N*log2N)

Сортирует входную последовательность с возможностью сохранения прежнего относительного порядка перемещенных равных элементов

Алгоритмы бинарного поиска в требуют, чтобы передаваемые в качестве операндов последовательности были отсортированы по некоторому критерию, в противном случае, они работают некорректно:

binary_search

С++ 98

O(log2N)

Бинарный поиск значения

equal_range

С++ 98

O(log2N)

Возвращает пару итераторов, соответствующую lower_bound и upper_bound

lower_bound

С++ 98

O(log2N)

Возвращает итератор на последнюю позицию в отсортированной последовательности, чье значение не меньше указанного

upper_bound

С++ 98

O(log2N)

Возвращает итератор на первую позицию в отсортированной последовательности, чье значение больше указанного

Алгоритмы обработки упорядоченных интервалов также требуют, чтобы передаваемые в качестве операндов последовательности были отсортированы по некоторому критерию:

includes

С++ 98

O(N)

Поиск вхождения отсортированной подпоследовательности в другую отсортированную последовательность

inplace_merge

С++ 98

O(N*log2N)

Осуществляет слияние двух упорядоченных подпоследовательностей, идущих подряд, в единую упорядоченную последовательность

merge

С++ 98

O(N)

Осуществляет слияние двух отсортированных последовательностей в третью выходную

set_difference

С++ 98

O(N)

Разница между двумя множествами, результат - в третьем

set_intersection

С++ 98

O(N)

Пересечение двух множеств в третье

set_symmetric_difference

С++ 98

O(N)

Симметрическая разница между двумя множествами, результат - в третьем

set_union

С++ 98

O(N)

Объединение двух множеств в третье

К алгоритмам работы с двоичными кучами относятся следующие:

is_heap

C++ 11

O(N)

Проверяет является ли интервал двоичной кучей

is_heap_until

C++ 11

O(N)

Возвращает итератор на позицию первого элемента, нарушающего порядок двоичной кучи

make_heap

С++ 98

O(N)

Переупорядочивает элементы последовательности таким образом, что в результате они формируют двоичную кучу

push_heap

С++ 98

O(log2N)

Помещает новое значение в двоичную кучу

pop_heap

С++ 98

O(log2N)

Удаляет элемент с наибольшим значением из кучи

sort_heap

С++ 98

O(N*log2N)

Превращает последовательность в виде двоичной кучи в отсортированную по возрастанию

К алгоритмам поиска минимума-максимума относятся следующие:

is_permutation

С++ 11

O(N2)

Определение является ли первая указанная последовательность перестановкой другой указанной последовательности

lexicographical_compare

C++98

O(N)

Лексикографическое (алфавитное) сравнение двух указанных последовательностей

max_element

С++ 98

O(N)

Поиск максимального значения в последовательности

min_element

С++ 98

O(N)

Поиск минимального значения в последовательности

minmax_element

С++ 11

O(N)

Поиск пары граничных значений в последовательности (минимум и максимум сразу)

next_permutation

С++ 98

O(N)

Переупорядочивает элементы последовательности таким образом, чтобы сформировать следующую по порядку лексикографическую перестановку

prev_permutation

С++ 98

O(N)

Переупорядочивает элементы последовательности таким образом, чтобы сформировать предыдущую по порядку лексикографическую перестановку

Наконец, численные методы специализируются на вычислительных операциях на указанных последовательностях:

accumulate

С++ 98

O(N)

Вычисляет сумму элементов последовательности

adjacent_difference

С++ 98

O(N)

Формирует выходную последовательность как разницу между текущим и предыдущим элементом

inner_product

С++ 98

O(N)

Вычисляет скалярное произведение элементов

iota

C++ 11

O(N)

Увеличивает каждый элемент последовательности на 1

partial_sum

С++ 98

O(N)

Формирует в выходной последовательности список частичных сумм предыдущих элементов входной

С каждым обновлением стандарта набор алгоритмов пополняется. Часто алгоритмы перегружены в нескольких вариантах, и количество перегрузок увеличивается для обеспечения новых задач.

Алгоритм std::for_each

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

Пусть требуется вывести на экран через пробел все элементы последовательности. Достаточно написать функцию, которая выводит конкретный элемент на экран и символ-пробел, а затем применить данную функцию в качестве аргумента алгоритма for_each:

#include <algorithm>

#include <iostream>

#include <vector>

#include <string>

// Функция для вывода значения в стандартный поток

template< typename T >

void print_value ( const T & _value )

{

std::cout << _value << ' ';

}

int main ()

{

// Массив целых чисел

int arr[ 10 ] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Выводим на экран весь массив при помощи std::for_each

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

// Вектор строк

std::vector< std::string > vs;

vs.push_back( "Hello" );

vs.push_back( "future" );

vs.push_back( "C++" );

vs.push_back( "programmer!" );

// Выводим на экран весь вектор при помощи std::for_each

std::for_each( vs.begin(), vs.end(), & print_value< std::string > );

std::cout << std::endl;

}

В результате получим на экране следующий вывод:

Алгоритм for_each также можно использовать для модификации последовательности. До появления стандарта С++’11 алгоритма iota не существовало, и осуществить увеличение всех элементов на 1 представлялось возможным при помощи алгоритма for_each, модифицирующего последовательность - достаточно в функции-обработчике (add_one) принимать аргумент по ссылке с правом на запись:

#include <algorithm>

#include <iostream>

// Функция для вывода значения в стандартный поток

template< typename T >

void print_value ( const T & _value )

{

std::cout << _value << ' ';

}

// Функция для добавления единицы к переданному элементу

template< typename T >

void add_one ( T & _ref )

{

_ref += 1;

}

int main ()

{

// Массив из 10 целых чисел

int arr[ 10 ] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Выводим в начальном состоянии

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

// Применяем add_one к каждому элементу массива

std::for_each( arr, arr + 10, & add_one< int > );

// Выводим в конечном состоянии

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

}

В результате получим следующий вывод на экране:

Алгоритмы “по месту” и алгоритмы с копированием

Некоторые алгоритмы стандартной библиотеки осуществляют подобные действия, с той разницей, что в одних случаях модификация элементов, или их фильтрация, или переупорядочивание происходят в оригинальной последовательности (inplace, “по месту”), а в других вариантах создается другая выходная последовательность-результат, при этом оригинал не модифицируется (copy).

Ниже приведен пример, демонстрирующий разницу между inplace- и copy-версиями алгоритмов на примере циклического вращения (rotate). В начале создается массив, затем используется copy-версия алгоритма для заполнения другого массива результатами вращения, при этом оригинальный массив не изменяется. Затем к оригинальному массиву применяется inplace-версия того же алгоритма, в результате чего его содержимое циклически сдвигается.

#include <algorithm>

#include <iostream>

template< typename T >

void print_value ( const T & _value )

{

std::cout << _value << ' ';

}

int main ()

{

// Оригинальный массив

int arr[ 10 ] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Выводим оригинальный массив

std::cout << "Original array: ";

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

//--------- Эксперимент 1 Copy-версия ------------

// Применяем copy-версию rotate, результат - во второй массив

int arrCopy[ 10 ];

std::rotate_copy( arr, arr + 5, arr + 10, arrCopy );

// Выводим оригинальный массив, он остался неизменным

std::cout << "Original array after copy transformation: ";

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

// Выводим массив-копию, в котором данные циклически сдвинуты относительно середины

std::cout << "Copy array: ";

std::for_each( arrCopy, arrCopy + 10, & print_value< int > );

std::cout << std::endl;

//--------- Эксперимент 2 Inplace-версия ------------

// Применяем inplace-вариант rotate, результат влияет на оригинал

std::rotate( arr, arr + 5, arr + 10 );

// Выводим массив, данные в нем циклически сдвинуты относительно середины

std::cout << "Original array after in-place transformation: ";

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

}

Особое внимание следует уделить inplace-версиям алгоритмов, название которых подразумевает удаление элементов из контейнера, в частности, семейства алгоритмов remove и unique. Напомним, что алгоритмы манипулируют данными контейнеров исключительно через предусмотренные итераторами операции. Ни одна из категорий итераторов не содержит операции наподобие “erase”. Если целевой контейнер является простым массивом фиксированного размера, то и реализовать какую-либо функциональность, связанную с уменьшением его размера, принципиально невозможно.

Что же происходит в таком случае вместо удаления? Такие алгоритмы просто перемещают данные последовательностей на позиции удаляемых элементов, фактически сдвигая границу последнего действительного элемента в сторону начала последовательности. Более того, такие алгоритмы возвращают итератор, указывающий на новую конечную границу действительной части последовательности. Данные, идущие после такой последовательности следует считать недействительными, а все данные, остающиеся в последовательности после выполнения алгоритма будут перемещены в область до этой позиции:

#include <algorithm>

#include <iostream>

template< typename T >

void print_value ( const T & _value )

{

std::cout << _value << ' ';

}

int main ()

{

int arr[ 10 ] = { 0, 1, 1, 1, 1, 2, 3, 4, 4, 5 };

int * ptr = std::unique( arr, arr + 10 );

std::cout << "Original array: ";

std::for_each( arr, arr + 10, & print_value< int > );

std::cout << std::endl;

std::cout << "Valid part: ";

std::for_each( arr, ptr, & print_value< int > );

std::cout << std::endl;

}

Изобразим преобразование графически:

Таким образом, inplace-версии алгоритмов с удалением перемещают остающиеся в последовательности данные и сужают действительный интервал вместо фактического удаления элементов, что является невозможным в общем случае для всех видов контейнеров.

Алгоритмы с пользовательскими условиями

Часть алгоритмов позволяет пользователю внедрять собственные условия в общий шаблон алгоритма для адаптации их поведения к нуждам решаемой задачи. В частности, для многих алгоритмов существует версия с суффиксом “_if”. В большинстве случаев условия представляют собой ПРЕДИКАТЫ - так в математике называют функции от N-переменных, возвращающие логическое значение true или false. Чаще всего используются унарные и бинарные предикаты, т.е. условия, возвращающие результат по одному и по двум аргументам соответственно.

В качестве условия могут быть использованы различные языковые конструкции, подходящие под шаблон предиката. В целом, алгоритмы используют предикаты аналогично вызовам функций. Соответственно, в простейшем случае в качестве предикатов могут использоваться функции, принимающие нужное количество входных данных через аргументы и возвращающие bool.

Приведем пример реализации алгоритмов count и count_if и сравним их друг с другом. Обычный вариант (count) ожидает получить от пользовательского кода данное-образец для сравнения. Этот образец используются для сравнения с очередным извлекаемым данным при помощи оператора ==. Подразумевается, что используемый тип данных допускает/предоставляет такой оператор:

template< typename InputIt, typename T >

size_t count ( InputIt first, InputIt last, T sample )

{

size_t result = 0;

while ( first != last )

{

// Сравниваем элемент последовательности с образцом через оператор ==

if ( * first == sample )

++ result;

++ first;

}

return result;

}

Вариант с пользовательским условием (count_if) возлагает на внешний предикат задачу установления нужно ли засчитывать очередной элемент в счетчике. Т.е., такой алгоритм абстрагируется от принципа сравнения значений с образцами и делегирует данную обязанность внешнему коду, который он автоматически вызывает в нужные моменты, передавая данные:

template< typename InputIt, typename UnaryPred >

size_t count_if ( InputIt first, InputIt last, UnaryPred up )

{

size_t result = 0;

while ( first != last )

{

// Выясняем нужно ли засчитывать данный элемент у внешнего унарного предиката

if ( up( * first ) )

++ result;

++ first;

}

return result;

}

Такой адаптируемый алгоритм можно использовать для подсчета количества элементов, соответствующих любому интересующему условию. Достаточно записать функцию-предикат, задающую интересующее условие, и алгоритм подсчета успешно справится с задачей.

В приведенном ниже примере программа подсчитывает количество чисел, хранящихся в векторе, которые делятся без остатка на 2. Для задания условия “делимости числа на 2” создается специальная функция dividesOnTwo, и ее адрес указывается в качестве предиката для стандартного алгоритма count_if:

#include <iostream>

#include <vector>

#include <algorithm>

// Функция-предикат: возвращает true, если число делится на 2 без остатка

bool dividesOnTwo ( int x )

{

return x % 2 == 0;

}

int main ()

{

// Формируем вектор с тестовыми данными: 0 1 2 3 4 5 6 7 8 9

std::vector< int > v;

for ( int i = 0; i < 10; i++ )

v.push_back( i );

// Считаем количеств чисел, делящихся на 2 без остатка.

int nOdd = std::count_if( v.begin(), v.end(), & dividesOnTwo );

// ^

// Задаем собственное условие-функцию

std::cout << "Number of elements that divide on 2 is: " << nOdd << std::endl;

}

Ниже приведен результат вывода данной программы: