// имеется конструктор по умолчанию
Widget w();// Какая неприятность...
Вместо объекта класса Widget с именем w в этом фрагменте объявляется функция w, которая вызывается без параметров и возвращает Widget. Умение распознавать подобные «ляпы» — признак хорошей квалификации программиста С++.
Все это по-своему интересно, однако мы нисколько не приблизились к поставленной цели: инициализировать объект list<int> содержимым файла. Зато теперь мы знаем, в чем заключается суть проблемы, и легко справимся с ней. Объявления формальных параметров не могут заключаться в круглые скобки, но никто не запрещает заключить в круглые скобки аргумент при вызове функции, поэтому простое добавление круглых скобок поможет компилятору увидеть происходящее под нужным углом зрения:
list<int> data((istream_iterator<int>(dataFile)), // Обратите внимание istream_iterator<int>()); // на круглые скобки
// вокруг первого аргумента
// конструктора list
Именно так следует объявлять данные. Учитывая практическую полезность istream_iterator и интервальных конструкторов (совет 5), этот прием стоит запомнить.
К сожалению, не все компиляторы знают об этом. Из нескольких протестированных компиляторов почти половина соглашалась только на
Более грамотный выход заключается в том, чтобы отказаться от модного использования анонимных объектов 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 соответственно); и, конечно же, они автоматически уничтожают все хранящиеся в них объекты при уничтожении самого контейнера.
Работая с такими интеллектуальными контейнерами, многие программисты вообще забывают о необходимости «прибрать за собой» и надеются, что контейнер выполнит за них всю грязную работу. Нередко их ожидания оправдываются, но если контейнер содержит
В результате при выполнении следующего фрагмента возникает утечка ресурсов:
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. За удаление таких элементов отвечает программист, а не контейнер. Так было задумано. Только программист знает,
Обычно это делать нужно. На первый взгляд решение выглядит довольно просто:
void doSomethng() {
vector<Widget*> vwp;
... // Как прежде
for (vector<Widget*>::iterator =vwp.begin();
i != vwp.end();
++i)
delete *i;
}
Такое решение работает, если не проявлять особой разборчивости в трактовке этого понятия. Во-первых, новый цикл for делает примерно то же, что и foreach, но он не столь нагляден (совет 43). Во-вторых, этот код небезопасен по отношению к исключениям. Если между заполнением vwp указателями и вызовом delete произойдет исключение, это снова приведет к утечке ресурсов. К счастью, с обеими проблемами можно справиться.
Чтобы от foreach-подобного цикла перейти непосредственно к foreach, необходимо преобразовать delete в объект функции. С этим справится даже ребенок — если, конечно, вы найдете ребенка, который захочет возиться с STL:
template <typename Т>
struct DeleteObject:// В совете 40 показано,
public unary_function<const T*.void> { // зачем нужно наследование
void operator()(const Т* ptr) const
{
delete ptr;
}
};
Теперь становится возможным следующее:
void doSomething() {
//См. ранее
for_each(vwp.begin(),vwp.end(),DeleteObject<Widget>());
}
К сожалению, вам приходится указывать тип объектов, удаляемых DeleteObject (в данном примере Widget), а это раздражает, vwp представляет собой vector<Widget*> —
class SpecialString: public string{...};
Это рискованно, поскольку string, как и все стандартные контейнеры STL, не имеет виртуального деструктора, а открытое наследование от классов без виртуального деструктора относится к числу основных табу С++. Подробности можно найти в любой хорошей книге по С++. (В «Effective С++» ищите в совете 14.) И все же некоторые программисты поступают подобным образом, поэтому давайте разберемся, как будет вести себя следующий код:
void doSomething() {
deque<SpecialString*> dssp:
for_each(dssp.begin(),end(),// Непредсказуемое поведение! Удаление
DeleteObject<string>()); // производного объекта через указатель
// на базовый класс при отсутствии // виртуального деструктора
}
Обратите внимание: dssp объявляется как контейнер, в котором хранятся указатели SpecialString*, но автор цикла for_each сообщает DeleteObject, что он будет удалять указатели string*. Понятно, откуда берутся подобные ошибки. По своему поведению SpecialString имеет много общего со string, поэтому клиенту легко забыть, что вместо string он использует SpecialString.
Чтобы устранить ошибку (а также сократить объем работы для клиентов DeleteObject), можно предоставить компилятору возможность вычислить тип указания, передаваемого DeleteObject::operator(). Все, что для этого нужно, — переместить определение шаблона из DeleteObject в operator():
struct DeleteObject{// Убрали определение шаблона
// и базовый класс
template<typename Т>// Определение шаблона
void operator()(const Т* ptr) const
{
delete ptr;
}
};
Компилятор знает тип указателя, передаваемого DeleteObject:: operator(), поэтому мы можем заставить его автоматически создать экземпляр operator() для этого типа указателя. Недостаток подобного способа вычисления типа заключается в том, что мы отказываемся от возможности сделать объект DeleteObject адаптируемым (совет 40). Впрочем, если учесть, на какое применение он рассчитан, вряд ли это можно считать серьезным недостатком.