Чтение онлайн

ЖАНРЫ

Эффективное использование STL
Шрифт:

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
.

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

void doSomething {

vector<Widget*> vwp;

for (int i=0; i<SOME_MAGIC_NUMBER; ++i) vwp.push_back(new Widget);

 … // Использовать vwp

} // Здесь происходит утечка Widget!

Все элементы

vwp
уничтожаются при выходе
vwp
из области видимости, но это не изменяет того факта, что
delete
не вызывается для объектов, созданных оператором
new
. За удаление таких элементов отвечает программист, а не контейнер. Так было задумано. Только программист знает, нужно ли вызывать
delete
для этих указателей.

Обычно это делать нужно. На первый взгляд решение выглядит довольно просто:

void doSomethng {

 vector<Widget*> vwp;

 ... // Как прежде

 for (vector<Widget*>::iterator = vwp.begin; i != vwp.end; ++i)

delete *i;

}

Такое

решение работает, если не проявлять особой разборчивости в трактовке этого понятия. Во-первых, новый цикл
for
делает примерно то же, что и
for_each
, но он не столь нагляден (совет 43). Во-вторых, этот код небезопасен по отношению к исключениям. Если между заполнением
vwp
указателями и вызовом
delete
произойдет исключение, это снова приведет к утечке ресурсов. К счастью, с обеими проблемами можно справиться.

Чтобы от

for_each
– подобного цикла перейти непосредственно к
for_each
, необходимо преобразовать
delete
в объект функции. С этим справится даже ребенок — если, конечно, вы найдете ребенка, который захочет возиться с STL:

template <typename T>

struct DeleteObject: // В совете 40 показано,

 public unary_function<const T*, void> { // зачем нужно наследование

 void operator(const T* ptr) const {

delete ptr;

 }

};

Теперь становится возможным следующее:

void doSomething {

 … //См. ранее

 for_each(vwp.begin, vwp.end, DeleteObject<Widget>);

}

К сожалению, вам приходится указывать тип объектов, удаляемых

DeleteObject
(в данном примере
Widget
), а это раздражает,
vwp
представляет собой
vector<Widget*>
разумеется,
DeleteObject
будет удалять указатели
Widget*
! Подобные излишества не только раздражают, но и приводят к возникновению трудно обнаружимых ошибок. Допустим, кто-нибудь по случайности объявляет класс, производный от
string
:

class SpecialString: public string{...};

Это рискованно, поскольку

string
, как и все стандартные контейнеры STL, не имеет виртуального деструктора, а открытое наследование от классов без виртуального деструктора относится к числу основных табу C++. Подробности можно найти в любой хорошей книге по C++. (В «Effective C++» ищите в совете 14.) И все же некоторые программисты поступают подобным образом, поэтому давайте разберемся, как будет вести себя следующий код:

void doSomething {

 deque<SpecialString*> dssp;

 for_each(dssp.begin, end, // Непредсказуемое поведение! Удаление

DeleteObject<string>); // производного объекта через указатель

// на базовый класс при отсутствии

// виртуального деструктора

}

Обратите внимание:

dssp
объявляется как контейнер, в котором хранятся указатели
SpecialString*
, но автор цикла
for_each
сообщает
DeleteObject
, что он будет удалять указатели
string*
. Понятно, откуда берутся подобные ошибки. По своему поведению
SpecialString
имеет много общего со
string
, поэтому клиенту легко забыть, что вместо
string
он использует
SpecialString
.

Поделиться с друзьями: