Топ контрибуторов
loading
loading
Знаете ли Вы, что

Список полученных сертификатов находится на странице Вашего профиля. Сертификаты можно распечатать или разместить на Вашем сайте.

Лента обновлений
ссылка May 18 13:25
Комментарий от MAX200688:
А как же неявный вызов конструктора родительского клас...
ссылка May 17 20:56
Комментарий от motya:
И лишнюю точку с запятой поставить, чтобы народ сетовал
ссылка May 17 20:49
Комментарий от motya:
Программы не существует
ссылка May 17 07:00
Комментарий от IWind:
Вот это бред, а не вопрос, автор вопроса ты был под чем?
ссылка May 16 21:33
Комментарий от kaput182:
Ужасно сформулированное условие.
Статистика

Тестов: 153, вопросов: 8595. Пройдено: 433513 / 2126320.

Scope guard в C++ (shared_ptr в boost)

head tail Статья
категория
C++
дата15.05.2009
авторanalizer
голосов8

Уходя гаси всех (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). Но есть и минусы:

  1. BOOST_SCOPE_EXIT нельзя размещать в глобальной области, только внутри функций.
  2. Нельзя вызвать код в BOOST_SCOPE_EXIT до выхода из блока (например оказалась что созданный объект более не нужен).
  3. Само написание BOOST_SCOPE_EXIT довольно громоздко и вам порой приходится писать по три строчки ради того чтобы вызвать всего одну функцию.

Тем не менее во многих случаях альтернатив BOOST_SCOPE_EXIT просто нет.

Альтернатива есть

Называется альтернатива - boost::shared_ptr. В библиотеке Буст реализован класс (и не один) "умных указателей" которые автоматически удаляют память из под объекта, как только количество ссылок на него становится равным нулю. Т.е. в большинстве случаев - когда вы выходите из области видимости, где создали объект. Отличительной фишкой класса boost::shared_ptr является то, что для объекта вы можете указать функцию (или функтор) которая произведёт удаление объекта. По-умолчанию - это оператор delete, но вы всегда можете передать любую другую функцию, например CloseHandle или fclose. Простой пример - программа, которая читает буфер обмена (виндовый) и пишет его содержимое в файл (сразу извинюсь за виндовую направленность). Нам понадобится автоматически делать следующие вещи:

  1. Закрывать клипборд (вызов CloseClipboard без параметров)
  2. Разлочивать область данных клипборда (вызов GlobalUnlock с хэндлом клипборда в качестве параметра)
  3. Закрывать файл (вызов 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 - налицо следующие плюсы:

  1. Объекты shared_ptr можно создавать в глобальной области (например если хотите при выходе из программы сделать CoUninitialize или XMLPlatformUtils::Terminate)
  2. Вы также можете сделать shared_ptr полем класса
  3. Вы можете уничтожить объект не дожидаясь конца локальной области видимости, просто вызвав метод reset или же присвоив гарду другое значение

Конечно никуда не деться от того факта что в качестве функции которая отвечает за уничтожение объекта можно использовать только уже существующие функии. Что либо более сложное нужно либо оформлять в виде новой функции, либо писать внутри BOOST_SCOPE_EXIT.

Вроде бы все рассказал что знал... ну ещё конечно можно наверное сказать, что вызов OpenClipboard(0) можно делать так - shared_ptr<void>((void*)NULL, bind(OpenClipboard,HWND())).reset(), но делать такое я вам категорически не советую. Желаю чтобы и мне, и вам за утечками памяти гоняться больше не приходилось.

----

Дмитрий analizer Потапов, апрель 2009

Если Вам понравилась статья, проголосуйте за нее

Голосов: 8  loading...
md6   sgauStudent   clumsy   qwerty2341   c0nst   mexal   serj   Max_Bond