Темы, которые касаются не только тестов, IT и Quizful, вы можете создавать в новом разделе Обсуждения.

Вдобавок, появилась возможность комментировать профиль пользователя на странице профиля.

Надеемся, эти нововведения Вам понравятся.
Знаете ли Вы, что

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

Топ контрибуторов
loading
loading
Статистика

Тестов: 127, вопросов: 5126. Пройдено: 54990 / 186428.

Scope guard в C++ (shared_ptr в boost)

head tail Информация о статье
категория
C++
дата15.05.2009
авторanalizer
голосов6

Уходя гаси всех (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

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

Голосов: 6  loading...
md6   sgauStudent   clumsy   qwerty2341   c0nst   mexal  
Комментариев: 13
 slavikkk22.02.2010 | 18:28:24
Видимо, в предисловии были не очень опытные программисты, которые зачем-то решили изобрести бредовый код, который не прошел бы ни одного нормально ревью, а также забыли о профайлере. :)
ответить
 analizer22.02.2010 | 23:10:37
Вы когда-нибудь участвовали в айтсорсинговом проекте, в котором со стороны заказчика ещё учавствуют представители заказчика? Так вот, с той стороны могут быть пиндосы, чёрные и узкоглазые, но единственным негром, который обязан соблюдать процесс разработки и проходить ревью будете всё равно вы. Это и есть "предыстория предисловия".
ответить
 slavikkk23.02.2010 | 00:43:15
Говнокод она не объясняет, в любом случае. Ну а по поводу участия: я его принимаю в достаточно крупных коммерческих проектах, так что не надо делать попыток давить на что-то. Ваш язвительный стиль ведения бесед уже стал неинтересен. А, вроде, и возраст-то нормальный, чтобы пережить этот максимализм.
ответить
 analizer23.02.2010 | 06:44:19
Тогда что вы здесь делаете если "неинтересен"?
ответить
 xzero10.10.2009 | 15:10:06
assert-a не хватает на возвращаемое значение CloseHandle
Даже при наличии wrapper-а assert всё равно нужен
ответить
 analizer11.10.2009 | 08:00:15
Имхо, перебор. Если загнётся CloseHandle, то сделать с этим уже ничего нельзя будет. Как правило это будет означать что пока программа работала, кто-то перетёр значение открытого хэндла.
ответить
 xzero11.10.2009 | 11:36:52
assert поможет обнаружить "глупые" ошибки на этапе отладки. Изменение кода может привести к нарушению требуемой последовательности закрытия хендлов, ведь гуард не обеспечивает соблюдение последовательности. Так же, так как handle не скрыт, он может быть передан куда-то и там закрыт, assert позволит обнаружить данную проблему.
ответить
 analizer11.10.2009 | 12:45:38
Как раз наоборот. Если создавать гард сразу после захвата ресурса, то освобождены они будут в порядке строго обратном захвату.

>>Так же, так как handle не скрыт, он может быть передан куда-то и там закрыт, assert позволит обнаружить данную проблему.

Каким образом? Хэндл как правило передаётся по значению, поэтому никто не соизволит выставить его значение в NULL или INVALID_HADLE_VALUE
ответить
 tellurian09.09.2009 | 20:35:15
Всё же предпочитаю принцип минимализма – ни чего лишнего в коде! Удобство штука хорошая, но ради одного только shared_ptr цеплять буст это, на мой взгляд, перебор – стрельба из пушки по воробьям. Особенно когда ради компиляции пары строк кода на другой машине надо тащить гиговую библиотеку (имеются введу исходники)
ответить
 analizer10.09.2009 | 05:55:07
Минималистам - boost bcp в помощь
ответить
 dufa12.11.2009 | 10:50:56
ну есть std::auto_ptr. тока аккуратней с его оператором = ;)
ответить
 analizer12.11.2009 | 10:56:05
Вот за это я и не люблю std::auto_ptr. Разные его "аккуратности" типа неконстантности аргумента в конструкторе копирования и операторе присваивания не позволяют быть уверенным в написанном коде.
ответить
 dufa12.11.2009 | 11:06:39
ну в принципе там кода на сотню строк. не сложного. так что, чтобы не тащить буст, можно свой костыль наваять. да и без операторов присваивания/копирования можно обойтись по большому счету.
ответить
Добавить комментарий