Ручное управление ресурсами в низкоуровневом си-подобном коде на 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)
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; }
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; }
VEG Автор
09.06.2025 07:30Я же упомянул об этом прямо во втором предложении в статье:
Создание достойных RAII-врапперов для каждого используемого сишного API не всегда практично
Для полноты картины вам нужно учитывать объём кода всех созданных вами врапперов, а так же то, что они будут не везде применимы. Например, после
CreateProcess
у вас в структуреPROCESS_INFORMATION pi
будут сырые хендлыpi.hProcess
иpi.hThread
, которые нужно закрыть, и вот вам уже нужно писать свой враппер дляCreateProcess
. Так вы быстро скатитесь к разработке целого C++ фреймворка на все случаи жизни вместо С-подобного низкоуровневого кода, о котором идёт речь в статье.ZirakZigil
09.06.2025 07:30Так вам не нужна обёртка для каждого вызова, вам нужна обёртка для каждого способа освобождения ресурса (CloseHandle, RegCloseKey, LsaClose, NetApiBufferFree, CloseThreadpool, что там ещё из массового). Причём подавляющее большинство ресурсов освобождаются стандартно, без приколов а ля CreateProcess. Мне кажется, вы сильно переоцениваете количество работы, которое нужно сделать базово, закатывать всё винапи целиком в raii нужно примерно так никому. Никому, кроме майков, т.е., так что берите и пользуйтесь: https://github.com/microsoft/wil.
icerasp
09.06.2025 07:30Выглядит потрясающе. Честно говоря, еще со школьных времен, когда только-только начинал изучать программирование, никогда не понимал, почему в любой более-менее сложной программе на C/C++ освобождение ресурсов превращается в танцы с бубном. Неужели на столько лет существование языков никому из разработчиков стандартов в голову не приходило как-то упростить этот постоянно использующийся процесс?
HiItsYuri
09.06.2025 07:30Существует примерно с рождения языка - RAII. А если нужно "сходить" в С, тогда, как написали выше, писать врапперы на это дело.
А это бессистемный подход какой то.
cdriper
09.06.2025 07:30для начала надо понять, что нет такого зверя "C/C++"
после этого нормально выучить хотя бы основы C++, чтобы понять как легко и корректно управлять ресурсами
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; }
domix32
09.06.2025 07:30А откуда этот Handler взялся?
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; };
Kelbon
09.06.2025 07:30Предлагаю не изобретать того что написано невероятное количество раз и использовать готовое и продуманное
https://github.com/kelbon/logic_guardsVEG Автор
09.06.2025 07:30Вашему решению не хватает лаконичности и гибкости. Все те же задачи решаются простым
defer
из данной статьи, без введения лишних сущностей. Если вам нужен какой-то условныйdefer
, вы можете просто явно прописывать эти условия в самой отложенной лямбде. Это мало того, что требует меньше кода, так ещё и более гибко: можно одной переменной отменять сразу несколько отложенных лямбд, в отличие от вашего решения.Предложенный
defer
имеет очень простую концепцию. Его легко воспроизвести (всего десяток строк кода), легко усвоить (всего одно новое ключевое слово), и просто адаптировать прямо на месте под нужды вашей функции, добавляя любые условия как на месте вызоваdefer
(путём захватаcond
по значению), так и на месте выполнения лямбды (читая значения переменных по ссылкам).Kelbon
09.06.2025 07:30Если вы хотите подробного разбора, ну давайте. У меня scope_exit занимает буквально 3 строки, всё остальное сделано для удобства использования и максимальной эффективности.
scope_failure сделано отдельным как раз чтобы не писать самому эти булевы флаги, а в удобной и читаемой форме описывать свои действия
Не смотря на кажущееся сходство и простоту обоих решений (моего и вашего), у них есть серьёзные различия:
ваш defer:
не компилируется в constexpr функции: https://godbolt.org/z/o84P1fGMT
не является empty типом, если лямбда внутри empty, это важно если вы решите вдруг сохранить это как поле типа или, что важнее, неявно компилятор сохранит скоп екзит на фрейме корутины
не является агрегатом, так как имеет конструктор. Это в свою очередь приводит к тому, что у вас теряется агрегатная инициализация, а в случае вашего конструктора вообще появляется копирование. Значит если вы всё таки решите захватывать в лямбду что-то кроме ссылок (вы вроде бы специально под это сделали макрос), то у вас будет лишнее копирование, в лучшем случае лишний мув. В моей реализации не будет ни мува, ни копирования.
Более того, ваш скоп екзит игнорирует исключение из копирования при конструировании. То есть скоп екзит не гарантирует, что функция всё таки будет вызвана, а именно это он хотел гарантировать
игнорируется исключение при вызове в деструкторе
Все эти проблемы в моём решении учтены
В scope_failure вдаваться не буду, как там лендинги расставляются и почему оно так написано
navrocky
09.06.2025 07:30Сколько пафоса ) А вот с английским языком в описании надо поработать...
Kelbon
09.06.2025 07:30А где тут пафос? Каждый день в течение многих лет появляются статьи, как кто-то опять написал scope_exit, зачастую куда хуже, чем написано в статье тут. С std::function или ещё чем-то таким
DjUmnik
09.06.2025 07:30Что будет, если f бросит исключение?
Макросы - зло.
VEG Автор
09.06.2025 07:30Что будет, если f бросит исключение?
То же самое, что и в случае исключения в любом деструкторе. Не стоит бросать исключения в деструкторах =)
Макросы - зло.
Это правда, но небольшая щепотка, которая облегчает жизнь, вполне допустима. Тем более в C-подобном коде, где ими и так щедро усыпано.
DjUmnik
09.06.2025 07:30Ваш макрос defer конфликтует с Boost.Asio.
VEG Автор
09.06.2025 07:30К сожалению, там используется шаблон под названием
defer
, поэтому макрос с таким же именем по понятным причинам несовместим с этим кодом. Можете попробовать заменить имя макроса наDEFER
или что-то другое, что не пересекается с вашим кодом.Как указано в первом абзаце статьи,
defer
хорош для низкоуровневого си-подобного кода. Boost в таком случае встречается редко, обычно нет поддержки исключений, часто нет полноценного STL, а иногда даже и стандартной библиотеки C. Если же у вас в проекте используются высокоуровневые библиотеки типа Boost.Asio, то наверное стоит и для всего остального использовать полноценные RAII-врапперы, либо же разделять высокоуровневый и низкоуровневый код по разным translation unit.
domix32
09.06.2025 07:30std::unique_ptr<std::remove_pointer_t<HANDLE>,decltype(&CloseHandle)>
Именно для этого и придуманы алиасы и авто. Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.
JordanCpp
09.06.2025 07:30Ну и как предложили выше можно сделать нормальную обёртку конкретно для виндовых структур.
Это будет оптимальнее. Хотя желание автора тоже понятно. Некий стандартный универсальный механизм.
JordanCpp
09.06.2025 07:30Мне не нравится вариант с unique_ptr тем, что будет по крайней мере один new. Банальный пример, а уже лезем в память. Возможно, что как то все оптимизируется. Я не вникал, как оно там устроено. Если, что поправьте.
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; }
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 правильно в своем первом комментарии указал про копирование лямбды. Это серьезный просчет.
Siemargl
09.06.2025 07:30И попробуй такую потенциальную утечку ресурсов затем найти.
А и вправду, объект ещё не создали)
eao197
09.06.2025 07:30Деструктор отработает
Чей деструктор? У HANDLE деструктора нет.
А объект, спрятанный за уродским макросом defer, появляется только после if-ов. И дело до этого объекта может не дойти.
majstar_Zubr
09.06.2025 07:30Что-то напоминает...
https://youtu.be/lKG1m2NkANM?si=BUjcnV5iJc6TLlXu
https://github.com/CppCon/CppCon2014/blob/master/Presentations/C%2B%2B11 in the Wild - Techniques from a Real Codebase/auto.h
VEG Автор
09.06.2025 07:30Как видите, решение настолько простое и прямо напрашивается, что другой человек написал бы почти то же самое, просто имея похожую идею в голове =)
Впрочем, перед тем, как писать свой вариант (несколько лет назад), я немного гуглил, и мне почему-то попадались примеры оверинжиниринга. Кто-то даже использовал
std::function
для хранения лямбды (что совершенно не бесплатно и генерирует кучу ненужного в данном случае кода).
navrocky
09.06.2025 07:30Не совсем понимаю в чем реализация полагается на C++17, тут C++11 должно быть достаточно.
И ещё можно улучшить, затащив [&] внутрь макроса, тогда можно будет писать почти как в Go:defer { close(file); };
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 _ =
Через несколько лет так и будем писать =)
navrocky
09.06.2025 07:30У меня аналогичный defer уже как много лет на 11 стандарте работал на многих моих проектах. Условный дефер не нужен, так как условие можно внутри блока написать. Поэтому не вижу смысла показывать что это лямбда. Семантика defer и так максмально прозрачна и понятна.
cdriper
09.06.2025 07:30люди стандартизируют вещи, чтобы через несколько лет вообще перестать писать макросы
gres_84
09.06.2025 07:30На прошлом проекте использовали для отработки действий при выходе из области видимости BOOST_SCOPE_EXIT_ALL. На текущем gsl::finally. Но тащить целую библиотеку для этого не всегда есть смысл, конечно.
navrocky
09.06.2025 07:30Раньше тоже использовал BOOST_SCOPE_EXIT, пока не появился 11 стандарт. Затем написал свой велосипед аналогичный этому defer. Так как это удобно.
Kelbon
Таких случаев нет
Также ваш тип копирует лямбду которую захватывает(а не мувает), что дополнительно создает проблемы
VEG Автор
Ого, как категорично!
Не помешало бы привести конкретный пример, демонстрирующий озвученные проблемы.
Kelbon
приведите пример, когда без явного захвата в scope exit у вас не получается что-то
я не знаю что тут можно демонстрировать, если делается копирование вместо мува, вам доказать, что некоторые типы не умеют копироваться или то что копировать это дороже чем мувать?
Siemargl
А что там копируется то, указатель на лямбду?
Надо бы по-хорошему в сгенерированный код глянуть.
Kelbon
мне лично не нужно смотреть в ассемблер, достаточно семантики языка. Если в лямбду захвачена строка, она скопируется, без ассемблера ясно
Siemargl
Это при захвате по ссылке?
Проверил в cpp insights, нет копирования, ссылка и все.
Кстати, лямбда тоже не копируется, инлайнит.
rblaze
Попробуйте передать в качестве параметра absl::AnyInvocable. Внезапно, он не копируется.