Книга: Эффективное использование STL
Совет 7. При использовании контейнеров указателей, для которых вызывался оператор new, не забудьте вызвать delete для указателей перед уничтожением контейнера
Совет 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
.
Чтобы устранить ошибку (а также сократить объем работы для клиентов DeleteObject
), можно предоставить компилятору возможность вычислить тип указания, передаваемого DeleteObject::operator()
. Все, что для этого нужно, — переместить определение шаблона из DeleteObject
в operator()
:
struct DeleteObject{ // Убрали определение шаблона
// и базовый класс
template<typename T> // Определение шаблона
void operator()(const T* ptr) const {
delete ptr;
}
};
Компилятор знает тип указателя, передаваемого DeleteObject::operator()
, поэтому мы можем заставить его автоматически создать экземпляр operator()
для этого типа указателя. Недостаток подобного способа вычисления типа заключается в том, что мы отказываемся от возможности сделать объект DeleteObject
адаптируемым (совет 40). Впрочем, если учесть, на какое применение он рассчитан, вряд ли это можно считать серьезным недостатком.
С новой версией DeleteObject
код клиентов SpecialString
выглядит так:
void doSomething() {
deque<SpecialString*> dssp;
...
for_each(dssp.begin(), dssp.end(),
DeleteObject());// Четко определенное поведение
}
Такое решение прямолинейно и безопасно по отношению к типам, что и требовалось.
Однако безопасность исключений все еще не достигнута. Если исключение произойдет после создания SpecialString
оператором new
, но перед вызовом for_each
, снова произойдет утечка ресурсов. Проблема решается разными способами, но простейший выход заключается в переходе от контейнера указателей к контейнеру умных указателей (обычно это указатели с подсчетом ссылок). Если вы незнакомы с концепцией умных указателей, обратитесь к любой книге по C++ для программистов среднего уровня и опытных. В книге «More Effective C++» этот материал приводится в совете 28.
Библиотека STL не содержит умных указателей с подсчетом ссылок. Написание хорошего умного указателя (то есть такого, который бы всегда правильно работал) — задача не из простых, и заниматься ею стоит лишь в случае крайней необходимости. Я привел код умного указателя с подсчетом ссылок в «More Effective C++» в 1996 году. Хотя код был основан на хорошо известной реализации умного указателя, а перед изданием книги материал тщательно проверялся опытными программистами, за эти годы было найдено несколько ошибок. Количество нетривиальных сбоев, возникающих при подсчете ссылок в умных указателях, просто невероятно (за подробностями обращайтесь к списку опечаток и исправлений для книги «More Effective C++» [28]).
К счастью, вам вряд ли придется создавать собственные умные указатели, поскольку найти проверенную реализацию не так сложно. Примером служит указатель shared_ptr
из библиотеки Boost (совет 50). Используя shared_ptr
, можно записать исходный пример данного совета в следующем виде:
void doSomething() {
typedef boost::shared_ptr<Widget> SPW; //SPW = "shared pointer
// to Widget"
vector<SPW> vwp;
for (int i=0; i<SOME_MAGIC_NUMBER; ++i) //Создать SPW no Widget*
vwp.push_back(SPW(new Widget)); //и вызвать push_back
… //Использовать vwp
} //Утечки Widget не происходит.
//даже если в предыдущем фрагменте
//произойдет исключение
Никогда не следует полагать, что автоматическое удаление указателей можно обеспечить созданием контейнера, содержащего auto_ptr
. Эта кошмарная мысль чревата такими неприятностями, что я посвятил ей совет 8.
Главное, что необходимо запомнить: контейнеры STL разумны, но они не смогут решить, нужно ли удалять хранящиеся в них указатели. Чтобы избежать утечки ресурсов при работе с контейнерами указателей, необходимо либо воспользоваться объектами умных указателей с подсчетом ссылок (такими, как shared_ptr
из библиотеки Boost), либо вручную удалить каждый указатель при уничтожении контейнера.
Напрашивается следующая мысль: если структура DeleteObject
помогает справиться с утечкой ресурсов для контейнеров, содержащих указатели на объекты, можно создать аналогичную структуру DeleteArray
, которая поможет избежать утечки ресурсов для контейнеров с указателями на массивы. Конечно, такое решение возможно. Другой вопрос, насколько оно разумно. В совете 13 показано, почему динамически размещаемые массивы почти всегда уступают vector
и string
, поэтому прежде чем садиться за написание DeleteArray
, пожалуйста, прочитайте совет 13. Может быть, он убедит вас в том, что лучше обойтись без DeleteArray
.
- Совет 1. Внимательно подходите к выбору контейнера
- Совет 2. Остерегайтесь иллюзий контейнерно-независимого кода
- Совет 3. Реализуйте быстрое и корректное копирование объектов в контейнерах
- Совет 4. Вызывайте empty вместо сравнения size() с нулем
- Совет 5. Используйте интервальные функции вместо одноэлементных
- Совет 6. Остерегайтесь странностей лексического разбора C++
- Совет 7. При использовании контейнеров указателей, для которых вызывался оператор new, не забудьте вызвать delete для указателей перед уничтожением контейнера
- Совет 8. Никогда не создавайте контейнеры, содержащие auto_ptr
- Совет 9. Тщательно выбирайте операцию удаления
- Совет 10. Помните о правилах и ограничениях распределителей памяти
- Совет 11. Учитывайте область применения пользовательских распределителей памяти
- Совет 12. Разумно оценивайте потоковую безопасность контейнеров STL
- Арифметические операторы
- 1.2.4. Операторы и приоритеты
- Условные операторы
- State NEW packets but no SYN bit set
- 2. How to Apply These Terms to Your New Programs
- 1. Оператор Select – базовый оператор языка структурированных запросов
- Операция new
- Поразрядные операторы
- 3.5 Проблемы доступа при использовании нескольких протоколов
- Оператор цикла foreach
- 10. Заблуждения, которых не стоит допускать
- У некоторых сайтов адрес начинается с www. Что это обозначает?