Как в современном C++ передать лямбде владение некопируемым объектом, чтобы ресурс жил не дольше самой лямбды?
Используйте инициализируемый захват лямбды и переместите объект в захват. Для некопируемого ресурса, например std::unique_ptr, это позволяет сделать лямбду владельцем ресурса без копирования.
Такой синтаксис появился в C++14. Захваченный объект становится членом объекта-замыкания, поэтому его время жизни связано с временем жизни лямбды.
В C++11 лямбды умели захватывать уже существующие локальные переменные по значению или по ссылке. Захват по значению требовал копирования переменной, что не подходило для std::unique_ptr и других некопируемых типов.
В C++14 появился инициализируемый захват: при создании замыкания можно сразу инициализировать его внутренний член произвольным выражением. Это решило задачу передачи владения и позволило лямбдам естественно работать с перемещаемыми ресурсами.
Захват ресурса по ссылке опасен, если лямбда переживёт локальную переменную: после выхода из функции ссылка станет висячей. Захват по значению безопаснее по времени жизни, но для некопируемого объекта он невозможен обычным копированием.
Замена std::unique_ptr на std::shared_ptr устраняет проблему копирования, но меняет модель владения: ресурс становится разделяемым, появляются счётчик ссылок и дополнительная стоимость. Если владение должно оставаться единственным, нужен именно перемещаемый захват.
Инициализируемый захват имеет форму имя = выражение. Если выражение содержит std::move, объект создаётся внутри замыкания перемещением, а не копированием:
Внутри замыкания появляется член, аналогичный resource, инициализированный из локального resource. После перемещения локальный указатель больше не владеет объектом, а лямбда становится его единственным владельцем.
Обычный operator() лямбды является const. Поэтому захваченный по значению объект нельзя изменять внутри тела такой лямбды; для изменения состояния нужен спецификатор mutable. Это не требуется, если ресурс только читается или передаётся дальше.
Замыкание с std::unique_ptr само становится некопируемым, поскольку его член некопируем. При этом оно обычно остаётся перемещаемым. Это ограничивает выбор контейнеров и обёрток: например, std::function требует копируемый вызываемый объект, тогда как std::move_only_function из C++23 предназначен для перемещаемых, но некопируемых вызываемых объектов.
Важно отличать перемещение от продления времени жизни. std::move лишь разрешает использовать объект как источник перемещения; время жизни ресурса продлевается потому, что сам объект-владелец создаётся внутри замыкания.
Фоновая задача должна использовать дескриптор файла после возврата функции, которая её создаёт. Захват дескриптора по ссылке создаёт риск обращения к уничтоженной переменной. Захват по значению не подходит, если дескриптор представлен некопируемым RAII-объектом.
Можно заменить дескриптор на std::shared_ptr, но это без необходимости вводит разделяемое владение и усложняет контроль момента освобождения. Можно передать сырой указатель, однако тогда лямбда не отвечает за время жизни ресурса.
Выбранное решение — переместить RAII-объект в инициализируемый захват. Лямбда получает единственное владение, задача безопасно переживает исходную функцию, а ресурс освобождается автоматически при уничтожении задачи. Если очередь задач копирует вызываемые объекты, вместо этого потребуется изменить интерфейс очереди, использовать разделяемое владение или выбрать специальную move-only-обёртку.
std::unique_ptr его автоматическое обнуление?Да, для перемещения std::unique_ptr исходный указатель становится пустым. Однако это свойство конкретного типа, а не общее правило для всех перемещаемых объектов: у произвольного типа после перемещения объект остаётся валидным, но его состояние может быть не определено точнее.
mutable?Лямбда без mutable имеет константный operator(). Её захваченные по значению члены внутри вызова рассматриваются как части константного объекта замыкания, поэтому изменение std::unique_ptr, например сброс владения или перемещение из него, запрещено.
mutable делает оператор вызова неконстантным и разрешает менять внутреннее состояние замыкания. Это не меняет способ захвата и не превращает захваченный объект в ссылку.
std::function?std::unique_ptr некопируем, поэтому замыкание, содержащее его по значению, тоже некопируемо. std::function рассчитан на копируемый целевой вызываемый объект и не может хранить такую лямбду в обычном сценарии.
Если копирование не нужно, применяют move-only-обёртку, например std::move_only_function из C++23, либо меняют интерфейс очереди так, чтобы он принимал и перемещал вызываемый объект. Использование std::shared_ptr совместимо с копированием, но представляет уже другую семантику владения.