Ручное управление ресурсами в низкоуровневом си-подобном коде на 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)
{
	bool result = false;
	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)
				{
					result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
					CloseHandle(file);
				}
				CloseHandle(proc);
			}
		}
		FreeLibrary(dbgdll);
	}

	return result;
}

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

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, и оно использует именно такой синтаксис.

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


  1. Kelbon
    09.06.2025 07:30

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

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

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


    1. VEG Автор
      09.06.2025 07:30

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

      Ого, как категорично!

      что дополнительно создает проблемы

      Не помешало бы привести конкретный пример, демонстрирующий озвученные проблемы.


      1. Kelbon
        09.06.2025 07:30

        Ого, как категорично!

        приведите пример, когда без явного захвата в scope exit у вас не получается что-то

        Не помешало бы привести конкретный пример, демонстрирующий озвученные проблемы.

        я не знаю что тут можно демонстрировать, если делается копирование вместо мува, вам доказать, что некоторые типы не умеют копироваться или то что копировать это дороже чем мувать?


        1. Siemargl
          09.06.2025 07:30

          А что там копируется то, указатель на лямбду?

          Надо бы по-хорошему в сгенерированный код глянуть.


          1. Kelbon
            09.06.2025 07:30

            Надо бы по-хорошему в сгенерированный код глянуть.

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


            1. Siemargl
              09.06.2025 07:30

              Это при захвате по ссылке?

              Проверил в cpp insights, нет копирования, ссылка и все.

              Кстати, лямбда тоже не копируется, инлайнит.


      1. rblaze
        09.06.2025 07:30

        Не помешало бы привести конкретный пример, демонстрирующий озвученные проблемы.

        Попробуйте передать в качестве параметра absl::AnyInvocable. Внезапно, он не копируется.


  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. belav
      09.06.2025 07:30

      А после

      return result;

      Надо вызывать CloseHandle(proc)?


      1. VEG Автор
        09.06.2025 07:30

        Вы правы, исправил пример. Спасибо за вашу внимательность =)


  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. VEG Автор
      09.06.2025 07:30

      Я же упомянул об этом прямо во втором предложении в статье:

      Создание достойных RAII-врапперов для каждого используемого сишного API не всегда практично

      Для полноты картины вам нужно учитывать объём кода всех созданных вами врапперов, а так же то, что они будут не везде применимы. Например, после CreateProcess у вас в структуре PROCESS_INFORMATION pi будут сырые хендлы pi.hProcess и pi.hThread, которые нужно закрыть, и вот вам уже нужно писать свой враппер для CreateProcess. Так вы быстро скатитесь к разработке целого C++ фреймворка на все случаи жизни вместо С-подобного низкоуровневого кода, о котором идёт речь в статье.


      1. ZirakZigil
        09.06.2025 07:30

        Так вам не нужна обёртка для каждого вызова, вам нужна обёртка для каждого способа освобождения ресурса (CloseHandle, RegCloseKey, LsaClose, NetApiBufferFree, CloseThreadpool, что там ещё из массового). Причём подавляющее большинство ресурсов освобождаются стандартно, без приколов а ля CreateProcess. Мне кажется, вы сильно переоцениваете количество работы, которое нужно сделать базово, закатывать всё винапи целиком в raii нужно примерно так никому. Никому, кроме майков, т.е., так что берите и пользуйтесь: https://github.com/microsoft/wil.


  1. icerasp
    09.06.2025 07:30

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


    1. HiItsYuri
      09.06.2025 07:30

      Существует примерно с рождения языка - RAII. А если нужно "сходить" в С, тогда, как написали выше, писать врапперы на это дело.

      А это бессистемный подход какой то.


    1. cdriper
      09.06.2025 07:30

      для начала надо понять, что нет такого зверя "C/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. domix32
          09.06.2025 07:30

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


  1. Kelbon
    09.06.2025 07:30

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

    https://github.com/kelbon/logic_guards


    1. VEG Автор
      09.06.2025 07:30

      Вашему решению не хватает лаконичности и гибкости. Все те же задачи решаются простым defer из данной статьи, без введения лишних сущностей. Если вам нужен какой-то условный defer, вы можете просто явно прописывать эти условия в самой отложенной лямбде. Это мало того, что требует меньше кода, так ещё и более гибко: можно одной переменной отменять сразу несколько отложенных лямбд, в отличие от вашего решения.

      Предложенный defer имеет очень простую концепцию. Его легко воспроизвести (всего десяток строк кода), легко усвоить (всего одно новое ключевое слово), и просто адаптировать прямо на месте под нужды вашей функции, добавляя любые условия как на месте вызова defer (путём захвата cond по значению), так и на месте выполнения лямбды (читая значения переменных по ссылкам).


      1. Kelbon
        09.06.2025 07:30

        Если вы хотите подробного разбора, ну давайте. У меня scope_exit занимает буквально 3 строки, всё остальное сделано для удобства использования и максимальной эффективности.

        scope_failure сделано отдельным как раз чтобы не писать самому эти булевы флаги, а в удобной и читаемой форме описывать свои действия

        Не смотря на кажущееся сходство и простоту обоих решений (моего и вашего), у них есть серьёзные различия:

        ваш defer:

        • не компилируется в constexpr функции: https://godbolt.org/z/o84P1fGMT

        • не является empty типом, если лямбда внутри empty, это важно если вы решите вдруг сохранить это как поле типа или, что важнее, неявно компилятор сохранит скоп екзит на фрейме корутины

        • не является агрегатом, так как имеет конструктор. Это в свою очередь приводит к тому, что у вас теряется агрегатная инициализация, а в случае вашего конструктора вообще появляется копирование. Значит если вы всё таки решите захватывать в лямбду что-то кроме ссылок (вы вроде бы специально под это сделали макрос), то у вас будет лишнее копирование, в лучшем случае лишний мув. В моей реализации не будет ни мува, ни копирования.

        • Более того, ваш скоп екзит игнорирует исключение из копирования при конструировании. То есть скоп екзит не гарантирует, что функция всё таки будет вызвана, а именно это он хотел гарантировать

        • игнорируется исключение при вызове в деструкторе

        Все эти проблемы в моём решении учтены

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


      1. stepsoft
        09.06.2025 07:30

        Ваше решение содержит недочеты и ошибки в реализации, исправление которых приведет к реализации @Kelbon.


    1. navrocky
      09.06.2025 07:30

      Сколько пафоса ) А вот с английским языком в описании надо поработать...


      1. Kelbon
        09.06.2025 07:30

        А где тут пафос? Каждый день в течение многих лет появляются статьи, как кто-то опять написал scope_exit, зачастую куда хуже, чем написано в статье тут. С std::function или ещё чем-то таким


  1. DjUmnik
    09.06.2025 07:30

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

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


    1. VEG Автор
      09.06.2025 07:30

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

      То же самое, что и в случае исключения в любом деструкторе. Не стоит бросать исключения в деструкторах =)

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

      Это правда, но небольшая щепотка, которая облегчает жизнь, вполне допустима. Тем более в C-подобном коде, где ими и так щедро усыпано.


      1. DjUmnik
        09.06.2025 07:30

        Ваш макрос defer конфликтует с Boost.Asio.


        1. VEG Автор
          09.06.2025 07:30

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

          Как указано в первом абзаце статьи, defer хорош для низкоуровневого си-подобного кода. Boost в таком случае встречается редко, обычно нет поддержки исключений, часто нет полноценного STL, а иногда даже и стандартной библиотеки C. Если же у вас в проекте используются высокоуровневые библиотеки типа Boost.Asio, то наверное стоит и для всего остального использовать полноценные RAII-врапперы, либо же разделять высокоуровневый и низкоуровневый код по разным translation unit.


  1. Playa
    09.06.2025 07:30

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


    1. VEG Автор
      09.06.2025 07:30

      Если очень повезёт, то в C++26, но скорее всего в C++29.


  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. Банальный пример, а уже лезем в память. Возможно, что как то все оптимизируется. Я не вникал, как оно там устроено. Если, что поправьте.



  1. vmx
    09.06.2025 07:30

    Классический goto cleanup на чистом C вроде не так пишут?

    Обычно он выглядит приблизительно так:

    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 loadlib_failed; }
    
    	pfnMiniDumpWriteDump = (decltype(&MiniDumpWriteDump)) GetProcAddress(dbgdll, "MiniDumpWriteDump");
    	if (!pfnMiniDumpWriteDump) { goto getprocaddr_failed; }
    
    	proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    	if (!proc) { goto openprocess_failed; }
    
    	file = CreateFileW(filename, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    	if (!file || file == INVALID_HANDLE_VALUE) { goto createfile_failed; }
    
    	result = pfnMiniDumpWriteDump(proc, pid, file, MiniDumpNormal, NULL, NULL, NULL);
    
    createfile_failed:
    	CloseHandle(file);
    
    openprocess_failed:
    	CloseHandle(proc);
    
    getprocaddr_failed:
    	FreeLibrary(dbgdll);
    
    loadlib_failed:
    	return result;
    }
    


  1. eao197
    09.06.2025 07:30

    У варианта со вкусом unique_ptr:

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

    есть то преимущество, что нет шансов добавить какой-то код между получением хэндла и созданием очищающего объекта для этого хэндла. У нас либо есть proc, для которой будет вызван деструктор, либо нет.

    Тогда как в варианте с defer:

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

    Запросто можно добавить еще один if перед defer и получить приключения:

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

    И попробуй такую потенциальную утечку ресурсов затем найти.

    ЗЫ. unique_ptr далеко не всегда подходит для очистки ресуров, для таких целей можно сделать небольшую универсальную обертку над разнотипными хэндлами. Вот здесь я когда-то давным-давно показывал пример класса handle_holder.

    ЗЗЫ. Ув.тов.Kelbon правильно в своем первом комментарии указал про копирование лямбды. Это серьезный просчет.


    1. Siemargl
      09.06.2025 07:30

      И попробуй такую потенциальную утечку ресурсов затем найти.

      А и вправду, объект ещё не создали)


      1. eao197
        09.06.2025 07:30

        Деструктор отработает

        Чей деструктор? У HANDLE деструктора нет.
        А объект, спрятанный за уродским макросом defer, появляется только после if-ов. И дело до этого объекта может не дойти.


  1. Jijiki
    09.06.2025 07:30

    wrl/comptr
    com-coding-practices
    так еще можно, наверно


  1. majstar_Zubr
    09.06.2025 07:30

    1. VEG Автор
      09.06.2025 07:30

      Как видите, решение настолько простое и прямо напрашивается, что другой человек написал бы почти то же самое, просто имея похожую идею в голове =)

      Впрочем, перед тем, как писать свой вариант (несколько лет назад), я немного гуглил, и мне почему-то попадались примеры оверинжиниринга. Кто-то даже использовал std::function для хранения лямбды (что совершенно не бесплатно и генерирует кучу ненужного в данном случае кода).


  1. navrocky
    09.06.2025 07:30

    Не совсем понимаю в чем реализация полагается на C++17, тут C++11 должно быть достаточно.

    И ещё можно улучшить, затащив [&] внутрь макроса, тогда можно будет писать почти как в Go:

    defer { close(file); };


    1. VEG Автор
      09.06.2025 07:30

      И ещё можно улучшить, затащив [&] внутрь макроса

      Об этом написано в самом конце статьи. С явным [&] выше гибкость (например, можно организовать условный defer, для которого необходимость выполнения вычисляется на месте defer), лучше наглядность (сразу понятно, что это лямбда, и return какого-то значения из неё никак не повлияет на результат внешней функции), и точка с запятой в конце имеет смысл (так как это defer callable;, а не некое подобие конструкции языка вроде if {}, где точка с запятой в конце не требуется). Ну и как вишенка на торте, когда я изначально делал этот макрос, был актуален пропозал добавить defer в Си, и он использовал именно такой синтаксис (но его, увы, не приняли, хотя как встроенная фича в Си он очень был бы кстати).

      Не совсем понимаю в чем реализация полагается на C++17, тут C++11 должно быть достаточно.

      Пришлось бы усложнять код, иначе не скомпилируется. В C++17 подтянули автовывод типов в шаблонах, плюс добавили гарантированный copy elision в некоторых случаях типа A a = A();. Кажется, с более старыми стандартами я не сталкивался уже более 5 лет. Нет смысла писать более сложный код под устаревшие компиляторы только потому, что это в принципе возможно.

      Если бы C++23 был уже в мейнстриме, я бы сразу в него и целился. Там вместо громоздкого:

      #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__) =
      

      можно было бы использовать недавно стандартизированный плейсхолдер _ и написать просто:

      #define defer deferrer _ =
      

      Через несколько лет так и будем писать =)


      1. navrocky
        09.06.2025 07:30

        У меня аналогичный defer уже как много лет на 11 стандарте работал на многих моих проектах. Условный дефер не нужен, так как условие можно внутри блока написать. Поэтому не вижу смысла показывать что это лямбда. Семантика defer и так максмально прозрачна и понятна.


      1. cdriper
        09.06.2025 07:30

        люди стандартизируют вещи, чтобы через несколько лет вообще перестать писать макросы


  1. gres_84
    09.06.2025 07:30

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


    1. navrocky
      09.06.2025 07:30

      Раньше тоже использовал BOOST_SCOPE_EXIT, пока не появился 11 стандарт. Затем написал свой велосипед аналогичный этому defer. Так как это удобно.


  1. DjUmnik
    09.06.2025 07:30

    Всё таки я не понимаю, чем это принципиально отличается от записи deferrer name([&]{...}); ? Неужели так трудно придумать имя? Зато можно добавить в темплейтный класс доп. функционал вроде условного выполнения (как у @Kelbonвыше) и не нужно загаживать код макросом.


    1. navrocky
      09.06.2025 07:30

      Именно чтобы меньше буков писать и нужны макросы.