Уходя гаси всех (c) народная мудрость
Рекомендуемая литература:
- Boost.SmartPtr
- Boost.ScopeExit
- Рекомендуемый к прохождению тест - Тест C++ - Средний уровень
Склероз
Вы когда-нибудь забывали освободить память из под временного объекта? Или закрыть виндовый хэндл? Или (что куда хуже) закрывали их в неправильном порядке?
Вот вам страшная сказка - однажды, в далёком 2006м году, четверо C++ программистов (трое из которых были опытными программистами, а четвёртым был я - студент-третьекурсник) в течении почти двух месяцев гонялись за серьёзным memory leak'ом (порядка мегабайта в час при сильной загрузке сервера) в огромном проекте из десятков модулей и многих мегабайт кода. Утечка конечно была найдена, но вот времени на её поимку было потрачено неимоверно много. Причина оказалась проста - создавался контекст шифрования, в этом контексте создавался ключ (получался его хэндл), производились манипуляции, а в конце функции закрывался хэндл контекста, а потом закрывался хэндл ключа... делал вид что закрывался. Возвращаемые значения функций закрывающих хэндлы, разумеется, не проверялись, и память утекала.
Как оградить себя от подобного? Да легко - сразу после успешного создания/открытия/инициализации объекта, достаточно создать в локальном блоке специальный класс который в деструкторе будет производить удаление/закрытие/деинициализацию. Например для хэндлов это будет выглядеть так:
#include <windows.h>
int main()
{
HANDLE hFile1 = CreateFile(...);
class HandleGuard{
HANDLE handle_;
public:
inline HandleGuard(HANDLE handle)
: handle_(handle){}
inline ~HandleGuard()
{
CloseHandle(handle_);
}
} hFile1Guard(hFile1);
HANDLE hFile2 = CreateFile(...);
HandleGuard hFile2Guard(hFile2);
// какие-то действия с файлами
return 0;
}
Вот! Теперь любые открытые хэндлы в C++ коде, для которых созданы guard'ы, будут закрыты при выходе из блока, причём в порядке обратном открытию эти хэндлов.
В примере выше есть один серьёзный недостаток - для каждой функции которая создаёт/открывает/инициализирует объект - вам придётся изобретать велосипед. На самом деле всё уже изобретено в библиотеке Boost. Осталось выбрать подходящее решение.
Очевидное решение
Баян - идея описанная ещё Птолемеем и его предшественниками.
... ну по крайней мере очевидное для тех кто читал обзор нововведений в Boost 1.38.0. Код выше можно переписать так:
#include <boost/scope_exit.hpp>
#include <windows.h>
int main()
{
HANDLE hFile1 = CreateFile(...);
BOOST_SCOPE_EXIT( (hFile1) )
CloseHandle(hFile1);
} BOOST_SCOPE_EXIT_END
HANDLE hFile2 = CreateFile(...);
BOOST_SCOPE_EXIT( (&hFile2) )
CloseHandle(hFile2);
} BOOST_SCOPE_EXIT_END
// какие-то действия с файлами
return 0;
}
Вот собственно и всё. После каждого открытия хэндла - вы создаёте новый блок BOOST_SCOPE_EXIT передавая ему нужные параметры, а при выходе из local scope - эти блоки будут исполняться в порядке обратном порядку создания. Перепутать последовательность станет куда сложнее.
Несомненным плюсом BOOST_SCOPE_EXIT является то, что в этом блоке можно разместить абсолютно любой код, без каких либо заморочек и особенностей (кроме особенностей любого local scope). Но есть и минусы:
BOOST_SCOPE_EXITнельзя размещать в глобальной области, только внутри функций.- Нельзя вызвать код в
BOOST_SCOPE_EXITдо выхода из блока (например оказалась что созданный объект более не нужен). - Само написание
BOOST_SCOPE_EXITдовольно громоздко и вам порой приходится писать по три строчки ради того чтобы вызвать всего одну функцию.
Тем не менее во многих случаях альтернатив BOOST_SCOPE_EXIT просто нет.
Альтернатива есть
Называется альтернатива - boost::shared_ptr. В библиотеке Буст реализован класс (и не один) "умных указателей" которые автоматически удаляют память из под объекта, как только количество ссылок на него становится равным нулю. Т.е. в большинстве случаев - когда вы выходите из области видимости, где создали объект.
Отличительной фишкой класса boost::shared_ptr является то, что для объекта вы можете указать функцию (или функтор) которая произведёт удаление объекта. По-умолчанию - это оператор delete, но вы всегда можете передать любую другую функцию, например CloseHandle или fclose. Простой пример - программа, которая читает буфер обмена (виндовый) и пишет его содержимое в файл (сразу извинюсь за виндовую направленность). Нам понадобится автоматически делать следующие вещи:
- Закрывать клипборд (вызов
CloseClipboardбез параметров) - Разлочивать область данных клипборда (вызов
GlobalUnlockс хэндлом клипборда в качестве параметра) - Закрывать файл (вызов
fcloseс указателем на файл в качестве параметра)
Итак приступим:
#include <stdio.h>
#include <windows.h>
#define BOOST_BIND_ENABLE_STDCALL
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/type_traits/remove_pointer.hpp>
using namespace boost;
int main()
{
OpenClipboard(0);
shared_ptr<void> openGuard((void*)NULL, bind(CloseClipboard));
HGLOBAL hClp = GetClipboardData(CF_TEXT);
const char* data = (const char*)GlobalLock(hClp);
shared_ptr<remove_pointer<HGLOBAL>::type> lockGuard(hClp, GlobalUnlock);
FILE* pf = fopen("clip.txt","w");
shared_ptr<FILE>(pf, fclose), fputs(data,pf);
lockGuard.reset();
}
Если разбить код выше на логические куски, то можно увидеть что каждый из них состоял из создания объекта и создания гарда, который отвечал за этого объекта уничтожение. Иногда даже имени для гарда не нужно придумывать - как в строке с fputs. Если кому-то станет интересно, почему это сначала делает запись в файл и только потом закрывает его - знайте мне тоже было интресно. Насчёт того, что можно было использовать ofstream вместо FILE - согласен, но здесь это использовано исключительно для наглядности. По сравнению с BOOST_SCOPE_EXIT - налицо следующие плюсы:
- Объекты
shared_ptrможно создавать в глобальной области (например если хотите при выходе из программы сделатьCoUninitializeилиXMLPlatformUtils::Terminate) - Вы также можете сделать
shared_ptrполем класса - Вы можете уничтожить объект не дожидаясь конца локальной области видимости, просто вызвав метод
resetили же присвоив гарду другое значение
Конечно никуда не деться от того факта что в качестве функции которая отвечает за уничтожение объекта можно использовать только уже существующие функии. Что либо более сложное нужно либо оформлять в виде новой функции, либо писать внутри BOOST_SCOPE_EXIT.
Вроде бы все рассказал что знал... ну ещё конечно можно наверное сказать, что вызов OpenClipboard(0) можно делать так - shared_ptr<void>((void*)NULL, bind(OpenClipboard,HWND())).reset(), но делать такое я вам категорически не советую. Желаю чтобы и мне, и вам за утечками памяти гоняться больше не приходилось.
----
Дмитрий analizer Потапов, апрель 2009
Даже при наличии wrapper-а assert всё равно нужен
>>Так же, так как handle не скрыт, он может быть передан куда-то и там закрыт, assert позволит обнаружить данную проблему.
Каким образом? Хэндл как правило передаётся по значению, поэтому никто не соизволит выставить его значение в NULL или INVALID_HADLE_VALUE