При проектировании очереди одноразовых задач, где callback может быть некопируемым, какую стандартную обёртку C++23 выбрать вместо std::function и почему?
Выберите std::move_only_function: она стирает тип вызываемого объекта, но сама перемещается, а не копируется. Поэтому очередь может хранить callback с некопируемым состоянием, например объектом, владеющим ресурсом или представляющим одноразовую операцию.
std::function для этого не подходит: её целевой объект должен быть копируемым, поскольку сама обёртка поддерживает копирование.
В C++11 появилась std::function как типобезопасная type-erasure-обёртка для хранения вызываемых объектов разных типов в одном интерфейсе. Её копируемость была удобна для обычных callback, но стала ограничением для перемещаемых задач.
В современном C++ появились вызываемые объекты, которые намеренно нельзя копировать: например, одноразовые задачи, объекты синхронизации и операции с уникальным владением. std::move_only_function стандартизирована в C++23 для такого сценария.
Очередь задач обычно должна принимать callback от вызывающего кода и хранить его до момента выполнения. Если callback содержит некопируемое состояние, попытка передать его в std::function приводит к ошибке компиляции.
Обходной путь через std::shared_ptr делает состояние копируемым, но меняет модель владения: задача уже не обязательно единолично владеет своим состоянием. Шаблонная функция может принимать любой callable без type erasure, однако тогда тип callback становится частью типа очереди или конкретного экземпляра алгоритма.
std::move_only_function использует стирание типа, поэтому очередь может иметь единый тип элемента, независимо от конкретного callable. При перемещении обёртки владение внутренним callable передаётся новому объекту; копирование запрещено.
Тип std::move_only_function<void()> задаёт вызываемую сигнатуру. В C++23 сигнатура может также фиксировать квалификаторы const, ref-квалификаторы и noexcept, поэтому требования к вызову выражаются непосредственно в типе обёртки.
Пустая std::move_only_function не должна вызываться: такое выполнение имеет неопределённое поведение. Это отличается от пустой std::function, вызов которой приводит к std::bad_function_call; перед вызовом нужно самостоятельно проверять состояние обёртки.
У std::move_only_function есть цена type erasure: возможны косвенный вызов и выделение памяти, зависящие от реализации и размера callable. Если конкретный тип callback известен на этапе компиляции, шаблонный интерфейс часто быстрее и позволяет лучше оптимизировать код, но не подходит для гетерогенного контейнера без дополнительной техники.
Сервис отложенного выполнения принимает задачи, содержащие одноразовые ресурсы. На первом варианте использовали std::function<void()>, но задачи с некопируемым состоянием не компилировались.
Рассматривались три решения:
std::shared_ptr состояния: совместимо с копированием, но добавляет разделяемое владение и лишнюю косвенность;Выбран третий вариант. Он соответствует одноразовой семантике задачи: постановка в очередь перемещает callback, а выполнение не требует его копирования. Дополнительно интерфейс явно показывает потребителям API, что callback должен передаваться с передачей владения.
Нет. Ссылка сама по себе не решает задачу хранения: обёртка должна либо владеть callable, либо ссылаться на объект с гарантированным временем жизни. При инициализации из временного некопируемого callable владение обычно перемещается внутрь std::move_only_function; при передаче ссылки можно получить callback, который станет висячим после уничтожения исходного объекта.
Она остаётся корректным объектом, но её состояние после перемещения не следует трактовать как конкретное содержимое. Нельзя полагаться на то, что она обязательно пуста, если это прямо не гарантировано используемым контрактом; безопасно проверять состояние перед вызовом и не использовать перемещённый-from объект как прежнюю задачу.
Шаблонный параметр подходит, когда тип callable известен в месте вызова, а type erasure не нужен. Такой подход устраняет косвенный вызов и часто даёт компилятору больше возможностей для инлайнинга, но разные типы задач нельзя напрямую поместить в один контейнер.
Если требуется гетерогенная очередь, стабильный ABI-подобный интерфейс или передача callback через границы модулей, std::move_only_function практичнее. Компромисс — возможные накладные расходы type erasure и необходимость явно учитывать неопределённое поведение при вызове пустой обёртки.