iterator контейнер::erase(iterator begin, iterator end);
В ассоциативных контейнерах сигнатура выглядит так:
void контейнер::erase(iterator begin, iterator end);
Чем обусловлены различия? Утверждается, что в ассоциативных контейнерах возврат итератора (для элемента, следующего за удаленным) привел бы к неприемлемому снижению быстродействия. Мне и многим другим это утверждение кажется сомнительным, но Стандарт есть Стандарт, а в нем сказано, что версии erase для последовательных и ассоциативных контейнеров обладают разными типами возвращаемого значения.
Многое из того, что говорилось в этом совете по поводу эффективности insert, относится и к erase. Интервальная форма erase также сокращает количество вызовов функций по сравнению с одноэлементной формой. При одноэлементном удалении элементы тоже сдвигаются на одну позицию к своему итоговой позиции, тогда как в интервальном варианте каждый элемент перемещается к итоговой позиции за одну операцию.
Но erase не присущ такой недостаток insert контейнеров vector и string, как многократные выделения памяти (конечно, для erase речь пойдет о многократном освобождении). Дело в том, что память, занимаемая vector и string, автоматически увеличивается для новых элементов, но при уменьшении количества элементов память не освобождается (в совете 17 рассказано о том, как уменьшить затраты освободившейся памяти в vector и string).
К числу особенно важных аспектов интервального удаления относится идиома erase-remove, описанная в совете 29.
•Интервальное присваивание. Как упоминалось в самом начале совета, во всех последовательных контейнерах предусмотрена интервальная форма assign:
void контейнер::assign(InputIterator begin, InputIterator end);
Итак, мы рассмотрели три веских аргумента в пользу применения интервальных функций вместо их одноэлементных аналогов. Интервальные функции обеспечивают более простую запись, они более четко выражают ваши намерения и обладают более высоким быстродействием. Против этого трудно что-либо возразить.
Совет 6. Остерегайтесь странностей лексического разбора С++
Предположим, у вас имеется файл, в который записаны числа типа int, и вы хотите скопировать эти числа в контейнер list. На первый взгляд следующее решение выглядит вполне разумно:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // Внимание! Эта строка
istream_iterator<int>()); // работает не так, как
// вы предполагали
Идея проста: передать пару istream_iterator интервальному конструктору list (совет 5), после чего скопировать числа из файла в список.
Программа будет компилироваться, но во время выполнения она ничего не сделает. Она не прочитает данные из файла. Она даже не создаст список — а все потому, что вторая команда не объявляет список и не вызывает конструктор. Вместо этого она... Произойдет нечто настолько странное, что я даже не рискну прямо сказать об этом, потому что вы мне не поверите. Вместо этого я попробую объяснить суть дела постепенно, шаг за шагом. Надеюсь, вы сидите? Если нет — лучше поищите стул...
Начнем с азов. Следующая команда объявляет функцию f, которая получает double и возвращает int:
int f(double d);
То же самое происходит и в следующей строке. Круглые скобки вокруг имени параметра d не нужны, поэтому компилятор их игнорирует:
int f(double(d));// То же,- круглые скобки вокруг d игнорируются
Рассмотрим третий вариант объявления той же функции. В нем просто не указано имя параметра:
int f(double);// То же; имя параметра не указано
Вероятно, эти три формы объявления вам знакомы, хотя о возможности заключать имена параметров в скобки известно далеко не всем (до недавнего времени я о ней не знал).
Теперь рассмотрим еще три объявления функции. В первом объявляется функция g с параметром — указателем на функцию, которая вызывается без параметров и возвращает double:
int g(double (*pf)()); // Функции g передается указатель на функцию
То же самое можно сформулировать и иначе. Единственное различие заключается в том, что pf объявляется в синтаксисе без указателей (допустимом как в С, так и в С++):
int g(double pf()); // То же; pf неявно интерпретируется как указатель
Как обычно, имена параметров могут опускаться, поэтому возможен и третий вариант объявления g без указания имени pf:
int g(double());// То же: имя параметра не указано
Обратите внимание на различия между круглыми скобками вокруг имени параметра (например, параметра d во втором объявлении f) и стоящими отдельно (как в этом примере). Круглые скобки, в которые заключено имя параметра, игнорируются, а круглые скобки, стоящие отдельно, обозначают присутствие списка параметров; они сообщают о присутствии параметра, который является указателем на функцию.
После небольшой разминки с объявлениями f и g мы возвращаемся к фрагменту, с которого начинается этот совет. Ниже он приводится снова:
list<int> data(istream_iterator<int>(dataFile),
istream_iterator<int>());
Держитесь и постарайтесь не упасть. Перед вами объявление функции data, возвращающей тип list<int>. Функция data получает два параметра:
•Первый параметр, dataFile, относится к типу istream_iterator<int>. Лишние круглые скобки вокруг dataFile игнорируются.
•Второй параметр не имеет имени. Он относится к типу указателя на функцию, которая вызывается без параметров и возвращает istream_iterator<int>.
Любопытно, не правда ли? Однако такая интерпретация соответствует одному из основных правил С++: все, что может интерпретироваться как указатель на функцию, должно интерпретироваться именно так. Каждый программист с опытом работы на С++ встречался с теми или иными воплощениями этого правила. Сколько раз вы встречались с такой ошибкой:
class Widget{...};// Предполагается, что у Widget
// имеется конструктор по умолчанию
Widget w();// Какая неприятность...
Вместо объекта класса Widget с именем w в этом фрагменте объявляется функция w, которая вызывается без параметров и возвращает Widget. Умение распознавать подобные «ляпы» — признак хорошей квалификации программиста С++.
Все это по-своему интересно, однако мы нисколько не приблизились к поставленной цели: инициализировать объект list<int> содержимым файла. Зато теперь мы знаем, в чем заключается суть проблемы, и легко справимся с ней. Объявления формальных параметров не могут заключаться в круглые скобки, но никто не запрещает заключить в круглые скобки аргумент при вызове функции, поэтому простое добавление круглых скобок поможет компилятору увидеть происходящее под нужным углом зрения:
list<int> data((istream_iterator<int>(dataFile)), // Обратите внимание istream_iterator<int>()); // на круглые скобки
// вокруг первого аргумента
// конструктора list
Именно так следует объявлять данные. Учитывая практическую полезность istream_iterator и интервальных конструкторов (совет 5), этот прием стоит запомнить.
К сожалению, не все компиляторы знают об этом. Из нескольких протестированных компиляторов почти половина соглашалась только на неправильное объявление data без дополнительных круглых скобок! Чтобы умиротворить такие компиляторы, можно закатить глаза и воспользоваться неверным, как было показано выше, объявлением data, но это недальновидное и плохо переносимое решение.
Более грамотный выход заключается в том, чтобы отказаться от модного использования анонимных объектов istream_iterator при объявлении data и просто присвоить этим итераторам имена. Следующий фрагмент работает всегда:
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin.dataEnd);
Именованные объекты итераторов противоречат стандартному стилю программирования STL, но зато ваша программа будет однозначно восприниматься как компиляторами, так и людьми, которые с ними работают.
Совет 7. При использовании контейнеров указателей, для которых вызывался оператор new, не забудьте вызвать delete для указателей перед уничтожением контейнера
Контейнеры STL отличаются умом и сообразительностью. Они поддерживают итераторы для перебора как в прямом, так и в обратном направлении (begin, end, rbegin и т. д.); они могут сообщить тип хранящихся в них объектов (value_type); они выполняют все необходимые операции управления памятью при вставке и удалении; они сообщают текущее количество элементов и максимальную вместимость (size и max_size соответственно); и, конечно же, они автоматически уничтожают все хранящиеся в них объекты при уничтожении самого контейнера.
Работая с такими интеллектуальными контейнерами, многие программисты вообще забывают о необходимости «прибрать за собой» и надеются, что контейнер выполнит за них всю грязную работу. Нередко их ожидания оправдываются, но если контейнер содержит указатели на объекты, созданные оператором new, этого не происходит. Разумеется, контейнер указателей уничтожает все хранящиеся в нем элементы при уничтожении самого контейнера, но «деструктор» указателя ничего не делает! Он не вызывает delete.