Ручное управление ресурсами в низкоуровневом си-подобном коде на C++ — довольно хлопотное занятие. Создание достойных RAII-врапперов для каждого используемого сишного API не всегда практично, а использование подходов с goto cleanup или множеством вложенных if (success) вредит читаемости кода.

Макрос defer, вдохновленный Go, как никогда кстати! Использовать его просто:

void* p = malloc(0x1000);
defer [&] { free(p); };

Отложенная лямбда будет выполнена при выходе из области видимости, независимо от того, будет ли выполнен return, брошено исключение (если разрешено), или даже выполнен goto наружу.

Реализация макроса лаконична и полагается на C++17 (Clang 5+, GCC 7+, MSVC 2017+):

#ifndef defer

template <typename T>
struct deferrer
{
	T f;
	deferrer(T f) : f(f) { };
	deferrer(const deferrer&) = delete;
	~deferrer() { f(); }
};

#define TOKEN_CONCAT_NX(a, b) a ## b
#define TOKEN_CONCAT(a, b) TOKEN_CONCAT_NX(a, b)
#define defer deferrer TOKEN_CONCAT(__deferred, __COUNTER__) =

#endif

Данный макрос по-настоящему zero-cost и не зависит от рантайма C или стандартной библиотеки, поэтому его можно использовать даже в разработке под ядро ОС.

Давайте сравним

Наивная версия

Представим функцию, где все выделенные ресурсы освобождаются явно при каждой ошибке:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
	HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
	if (!dbgdll)
	{
		return false;
	}

	auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
	if (!pfnMiniDumpWriteDump)
	{
		FreeLibrary(dbgdll);
		return false;
	}

	HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!proc)
	{
		FreeLibrary(dbgdll);
		return false;
	}

	HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (!file || file == INVALID_HANDLE_VALUE)
	{
		CloseHandle(proc);
		FreeLibrary(dbgdll);
		return false;
	}

	bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);

	CloseHandle(file);
	CloseHandle(proc);
	FreeLibrary(dbgdll);

	return result;
}

Выглядит плохо. Много продублированных строк кода, можно легко ошибиться и забыть освободить что-то.

Классический goto cleanup

Та же функция, но в классическом стиле goto cleanup:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
	bool result = false;
	HMODULE dbgdll = NULL;
	decltype(&MiniDumpWriteDump) pfnMiniDumpWriteDump = nullptr;
	HANDLE proc = NULL;
	HANDLE file = NULL;

	dbgdll = LoadLibraryA("dbghelp.dll");
	if (!dbgdll) { goto cleanup; }

	pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
	if (!pfnMiniDumpWriteDump) { goto cleanup; }

	proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!proc) { goto cleanup; }

	file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (!file || file == INVALID_HANDLE_VALUE) { goto cleanup; }

	result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);

cleanup:

	if (file && file != INVALID_HANDLE_VALUE)
	{
		CloseHandle(file);
	}

	if (proc)
	{
		CloseHandle(proc);
	}

	if (dbgdll)
	{
		FreeLibrary(dbgdll);
	}

	return result;
}

Нельзя перепрыгнуть через объявления переменных, поэтому приходится объявить их заранее. Этот код также немного менее эффективен, так как в cleanup повторно проверяется валидность значений, чтобы определить, какие из ресурсов нужно освобождать. Код освобождения находится далеко от кода выделения, так что легко не заметить ошибку, если вы забыли что-то освободить или сделали это в неправильном порядке.

Вложенные if (success)

При подходе с вложенными if (success) наша функция будет выглядеть так:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
	HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
	if (dbgdll)
	{
		auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
		if (pfnMiniDumpWriteDump)
		{
			HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
			if (proc)
			{
				HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
				if (file && file != INVALID_HANDLE_VALUE)
				{
					bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
					CloseHandle(file);
					return result;
				}
				CloseHandle(proc);
			}
		}
		FreeLibrary(dbgdll);
	}
	return false;
}

Уже лучше, но из-за излишней вложенности вам лучше иметь монитор пошире.

WTF std::unique_ptr

То же самое, но со вкусом STL:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
	std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)> dbgdll(LoadLibraryA("dbghelp.dll"), &FreeLibrary);
	if (!dbgdll) { return false; }

	auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll.get(), "MiniDumpWriteDump");
	if (!pfnMiniDumpWriteDump) { return false; }

	std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> proc(OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid), &CloseHandle);
	if (!proc) { return false; }

	std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> file([&]{
		auto h = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		return (h != INVALID_HANDLE_VALUE) ? h : NULL;
	}(), &CloseHandle);
	if (!file) { return false; }

	return pfnMiniDumpWriteDump(proc.get(), pid, file.get(), MiniDumpNormal, NULL, NULL, NULL);
}

STL, как всегда, даёт наилучший WTF-опыт. Некоторые люди действительно используют этот трюк с std::unique_ptr для автоматического освобождения не указателей, хотя автоматический вывод аргументов шаблона здесь не работает и требуется каждый раз указывать все эти многословные типы. Есть и важное ограничение: ресурс должен быть nullptr в невалидном состоянии, что не всегда так, из-за чего приходится использовать дополнительные хаки и трюки.

И, наконец, defer!

Мы можем переписать эту функцию с использованием макроса defer таким образом:

bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
{
	HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
	if (!dbgdll) { return false; }
	defer [&] { FreeLibrary(dbgdll); };

	auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
	if (!pfnMiniDumpWriteDump) { return false; }

	HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!proc) { return false; }
	defer [&] { CloseHandle(proc); };

	HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (!file || file == INVALID_HANDLE_VALUE) { return false; }
	defer [&] { CloseHandle(file); };

	return pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
}

Это выглядит гораздо лучше! Нет излишней вложенности, нет ненавистного goto, нет дублирования строк кода.

Почему именно такой синтаксис?

А как иначе оно могло бы выглядеть? Давайте посмотрим.

defer free(p);

Вариант в стиле Go. К сожалению, это невозможно реализовать в виде макроса для C++.

defer(free(p));

Это выглядит так, будто free(p) вызывается немедленно, и его результат передается в defer. Также это не позволяет помещать в defer несколько строк кода, что иногда полезно.

defer { free(p); };

Уже лучше, но мы не можем контролировать, хотим ли мы захватывать внешние переменные по значению или по ссылке, что важно в некоторых случаях.

defer [&] { free(p); };

Наш синтаксис. Он ожидает полноценную лямбду, обеспечивая необходимую гибкость, позволяя захватывать внешние переменные по ссылке или по значению. На самом деле, этот макрос может принять любой callable, а не только лямбды, так что даже точка с запятой после закрывающей фигурной скобки выглядит уместной.

Также существует предложение добавить defer в C, и оно использует именно такой синтаксис.

Комментарии (15)


  1. Kelbon
    09.06.2025 07:30

    Уже лучше, но мы не можем контролировать, хотим ли мы захватывать внешние переменные по значению или по ссылке, что важно в некоторых случаях.

    Таких случаев нет

    Также ваш тип копирует лямбду которую захватывает(а не мувает), что дополнительно создает проблемы


  1. JordanCpp
    09.06.2025 07:30

    Современные фичи С++ это хорошо, правильно. Но если уж исходить из юзабилити, вполне понятный вариант второй. Проверил, прочитал, закрыл. Никто же не мешает отделить LoadLibraryA и FreeLibrary в класс и дергать его в данном методе при выходе из функции сработает деструктор. Написать шаблон с HANDLE, который в деструкторе будет всегда CloseHandle(file) это делать.

    Помню, вроде на rsdn подобный код был, типа заворачиваем WinApi в ООП с конструкторами и деструкторами.

    bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
    {
    	HMODULE dbgdll = LoadLibraryA("dbghelp.dll");
    	if (dbgdll)
    	{
    		auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
    		if (pfnMiniDumpWriteDump)
    		{
    			HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    			if (proc)
    			{
    				HANDLE file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    				if (file && file != INVALID_HANDLE_VALUE)
    				{
    					bool result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
    					CloseHandle(file);
    					return result;
    				}
    				CloseHandle(proc);
    			}
    		}
    		FreeLibrary(dbgdll);
    	}
    	return false;
    }


  1. usrsse2
    09.06.2025 07:30

    defer { free(p); };

    Это как в Swift (кроме точек с запятой)


  1. JordanCpp
    09.06.2025 07:30

    Придумал, ещё вариант по старинке:) Класс Library и Handler дергают деструктор FreeLibrary и CloseHandle.

    bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
    {
    	Library dbgdll("dbghelp.dll");
    	if (dbgdll.Ok())
    	{
    		auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) dbgdll.GetAddress("MiniDumpWriteDump");
    		if (pfnMiniDumpWriteDump)
    		{
    			Handler<HANDLE> proc(procOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid));
    			if (proc.Ok())
    			{
    				Handler<HANDLE> file(CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
    				if (file.Ok() && file.Get() != INVALID_HANDLE_VALUE)
    				{
    					bool result = pfnMiniDumpWriteDump(proc.Get(), pid, file.Get(), MiniDumpNormal, NULL, NULL, NULL);
    					return result;
    				}
    			}
    		}
    	}
    
    	return false;
    }


  1. icerasp
    09.06.2025 07:30

    Выглядит потрясающе. Честно говоря, еще со школьных времен, когда только-только начинал изучать программирование, никогда не понимал, почему в любой более-менее сложной программе на C/C++ освобождение ресурсов превращается в танцы с бубном. Неужели на столько лет существование языков никому из разработчиков стандартов в голову не приходило как-то упростить этот постоянно использующийся процесс?


  1. JordanCpp
    09.06.2025 07:30

    Ещё вариант, без мам, пап и unique_ptr

    bool MakeDumpToFile(DWORD pid, PCWCHAR filename)
    {
    	Library dbgdll("dbghelp.dll");
    	if (!dbgdll.Ok()) return false;
    
    	auto pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) dbgdll.GetAddress("MiniDumpWriteDump");
    	if (!pfnMiniDumpWriteDump) return false;
    		
    	Handler<HANDLE> proc(procOpenProcess(PROCESS_ALL_ACCESS, FALSE, pid));
    	if (!proc.Ok()) return false;
    			
    	Handler<HANDLE> file(CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
    	if (!file.Ok() && file.Get() == INVALID_HANDLE_VALUE) return false;
    				
    	bool result = pfnMiniDumpWriteDump(proc.Get(), pid, file.Get(), MiniDumpNormal, NULL, NULL, NULL);
    	return result;
    }


    1. domix32
      09.06.2025 07:30

      А откуда этот Handler взялся?


      1. JordanCpp
        09.06.2025 07:30

        Примерно так, так как HANDLE используется чуть менее чем во всем WinAPI облегчит жизнь. Это общая идея. Таким образом типизировать другие типы. Добавить inline, но со сборкой O2 думаю и так компилятор догадается.

        template <typename T>
        class Handler
        {
        public:
        	Handler(T handle) :
        		_handle(handle)
        	{
        	}
        	~Handler()
        	{
        		if (_handle != nullptr) CloseHandle(_handle);
        	}
        
        	T Get()
        	{
        		return _handle;
        	}
        
        	bool Ok()
        	{
        		return _handle != nullptr;
        	}
        private:
        	T _handle;
        };


  1. Kelbon
    09.06.2025 07:30

    Предлагаю не изобретать того что написано невероятное количество раз и использовать готовое и продуманное

    https://github.com/kelbon/logic_guards


  1. DjUmnik
    09.06.2025 07:30

    Что будет, если f бросит исключение?

    Макросы - зло.


  1. Playa
    09.06.2025 07:30

    Вы только что scope_exit


  1. domix32
    09.06.2025 07:30

    std::unique_ptr<std::remove_pointer_t<HANDLE>,decltype(&CloseHandle)>

    Именно для этого и придуманы алиасы и авто. Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.


    1. JordanCpp
      09.06.2025 07:30

      Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.

      Это будет оптимальнее. Хотя желание автора тоже понятно. Некий стандартный универсальный механизм.


  1. JordanCpp
    09.06.2025 07:30

    Мне не нравится вариант с unique_ptr тем, что будет по крайней мере один new. Банальный пример, а уже лезем в память. Возможно, что как то все оптимизируется. Я не вникал, как оно там устроено. Если, что поправьте.