惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
N
News and Events Feed by Topic
S
Schneier on Security
G
GRAHAM CLULEY
Scott Helme
Scott Helme
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
GbyAI
GbyAI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
Cisco Talos Blog
Cisco Talos Blog
P
Privacy & Cybersecurity Law Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
Project Zero
Project Zero
MyScale Blog
MyScale Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
The Last Watchdog
The Last Watchdog
Vercel News
Vercel News
The Cloudflare Blog
C
Check Point Blog
Help Net Security
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
AI
AI
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
NISL@THU
NISL@THU
S
Security @ Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
S
Securelist
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
Vulnerabilities – Threatpost
S
Secure Thoughts

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Лямбды в C++: пять задач на захваты и время жизни, в которых ошибается даже опытный разработчик
artem · 2026-06-23 · via Все публикации подряд на Хабре

Сложный

11 мин

12

Привет, Хабр!

Лямбды в C++ существуют с 2011 года, и за пятнадцать лет вокруг них накопилось столько неочевидностей, что они до сих пор регулярно встречаются на код-ревью даже у senior-разработчиков. Захваты по ссылке, время жизни this, mutable-семантика, поведение в range-based for, ограничения std::function: каждая из этих тем выглядит элементарной в отрыве, но в боевом коде одна неосторожная лямбда превращается в undefined behavior, который не ловится компилятором и проявляется в продакшене через крэш или, что хуже, через тихое повреждение данных.

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

Задача 1: захват по ссылке локальной переменной

Функция возвращает лямбду, которая ссылается на локальное состояние:

#include <functional>
#include <iostream>

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() {
        return ++count;
    };
}

int main() {
    auto counter = make_counter();
    std::cout << counter() << '\n';
    std::cout << counter() << '\n';
    std::cout << counter() << '\n';
}

Программа компилируется без ошибок, без предупреждений в большинстве компиляторов, иногда даже выдаёт ожидаемые 1, 2, 3 в debug-сборке. Что в реальности происходит и почему вывод нельзя считать корректным?

Лямбда захватывает count по ссылке через [&count]. Локальная переменная count живёт в стек-фрейме функции make_counter до момента возврата из неё. После возврата стек-фрейм перестаёт существовать, и память, где лежал count, либо переиспользуется следующими вызовами, либо остаётся со старым значением до тех пор, пока никто не записал туда что-то новое. Лямбда внутри std::function теперь содержит висячую ссылку, обращение к ней при каждом вызове - counter() это undefined behavior.

В debug-сборке часто получается «корректный» вывод по простой причине: после возврата из make_counter стек-фрейм не затирается сразу, область памяти остаётся нетронутой до следующего вызова. В release-сборке с оптимизациями компилятор может переиспользовать ту же память для других переменных в main, и значения начнут вести себя случайным образом. Под санитайзером AddressSanitizer этот код падает с ошибкой stack-use-after-return и показывает точное место проблемы.

Решение: захватывать по значению, что копирует переменную в storage самой лямбды:

std::function<int()> make_counter() {
    int count = 0;
    return [count]() mutable {
        return ++count;
    };
}

Здесь count копируется в саму лямбду, mutable нужен, потому что по умолчанию лямбда с захватом по значению создаёт operator() с квалификатором const, и менять захваченные значения нельзя. С mutable всё работает: счётчик хранится внутри лямбды, переживает любые перемещения и копирования объекта std::function.

GCC 13+ и Clang 16+ с флагом -Wdangling-reference (включён в -Wall начиная с GCC 14) выдают предупреждение в подобных случаях, но реально срабатывает оно не везде. Поэтому правило простое: захватывать по ссылке, только если точно знаете, что лямбда не переживёт область видимости захваченных переменных.

Задача 2: захват this с переживанием объекта

Класс регистрирует callback на сервис уведомлений и забывает о нём:

#include <functional>
#include <string>
#include <vector>
#include <iostream>

class NotificationService {
public:
    void subscribe(std::function<void(const std::string&)> handler) {
        handlers_.push_back(std::move(handler));
    }

    void notify(const std::string& message) {
        for (const auto& h : handlers_) h(message);
    }

private:
    std::vector<std::function<void(const std::string&)>> handlers_;
};

class UserSession {
public:
    UserSession(NotificationService& service, std::string username)
        : username_(std::move(username)) {
        service.subscribe([this](const std::string& msg) {
            std::cout << username_ << " received: " << msg << '\n';
        });
    }

private:
    std::string username_;
};

int main() {
    NotificationService service;
    {
        UserSession alice(service, "alice");
        service.notify("hello");
    }
    service.notify("world");
}

Первый вызов notify работает корректно и выводит «alice received: hello». Второй вызов notify после уничтожения alice приводит к undefined behavior. Какое исправление архитектурно правильное и почему банальный захват username_ по значению вместо this решает не всю проблему?

Лямбда захватывает this, что фактически означает захват указателя на объект UserSession. Когда alice уничтожается в конце блока, указатель остаётся в NotificationService::handlers_, и при следующем notify через этот указатель идёт обращение к уничтоженному объекту.

Захват username_ по значению (через [username = username_] или просто [*this] начиная с C++17) убирает обращение к мёртвому объекту, но решает только верхний слой проблемы. Главная архитектурная проблема не в этом: callback продолжает жить и вызываться, хотя сущность, к которой он относился, уже мертва. В большинстве систем это не то поведение, которое хотел разработчик. Если alice вышла из сессии, она не должна получать уведомления.

Правильное решение использует механизм деподписки. Самый распространённый вариант в современном C++: weak_ptr вместе с shared_ptr плюс проверка валидности в начале callback.

class UserSession : public std::enable_shared_from_this<UserSession> {
public:
    static std::shared_ptr<UserSession> create(NotificationService& service, std::string username) {
        auto session = std::shared_ptr<UserSession>(new UserSession(std::move(username)));
        std::weak_ptr<UserSession> weak = session;
        service.subscribe([weak](const std::string& msg) {
            if (auto self = weak.lock()) {
                std::cout << self->username_ << " received: " << msg << '\n';
            }
        });
        return session;
    }

private:
    explicit UserSession(std::string username) : username_(std::move(username)) {}
    std::string username_;
};

Теперь callback держит weak_ptr, который не продлевает время жизни объекта. При вызове weak.lock() возвращается либо shared_ptr на живой объект (и callback выполняется), либо пустой shared_ptr (и callback тихо пропускается). Эта схема стандартная для асинхронных систем на C++ и встречается в Boost.Asio, Folly, Qt и многих других местах.

Альтернативный подход без shared_ptr: возвращать из subscribe какой-то id и иметь явный метод unsubscribe, который вызывается в деструкторе подписчика.

Задача 3: mutable lambda копирует, а не разделяет состояние

Сценарий с распределением работы между несколькими worker-потоками. Каждый поток получает свою копию лямбды, но логически счётчик должен быть один:

#include <iostream>
#include <thread>
#include <vector>

void run_workers() {
    int processed = 0;
    auto worker = [processed]() mutable {
        for (int i = 0; i < 100; ++i) {
            ++processed;
        }
        std::cout << "Worker processed: " << processed << '\n';
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(worker);
    }
    for (auto& t : threads) t.join();

    std::cout << "Total processed: " << processed << '\n';
}

Каждый из пяти worker-потоков выводит «Worker processed: 100», в финале «Total processed: 0». Разработчик ожидал увидеть 500 в финале (пять воркеров обработали по 100 элементов). Что пошло не так и какие два решения тут есть, в зависимости от того, что вообще требовалось?

mutable lambda хранит захваченные переменные внутри объекта самой лямбды. Когда лямбда передаётся в std::thread или копируется в threads.emplace_back(worker), копируется и её внутреннее состояние. Каждый поток получает свою независимую копию processed, увеличивает её до 100 и завершается. Переменная processed в run_workers - это совсем другая переменная, которая не менялась ни разу, потому что захват был по значению.

Решение зависит от того, что хотел программист.

Если задача состояла в том, чтобы каждый воркер посчитал свою порцию работы независимо, никакой проблемы и нет, всё работает корректно. В финале нужно либо собрать результаты от воркеров через future, либо использовать atomic-счётчик:

#include <atomic>

void run_workers() {
    std::atomic<int> processed{0};
    auto worker = [&processed]() {
        for (int i = 0; i < 100; ++i) {
            processed.fetch_add(1, std::memory_order_relaxed);
        }
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(worker);
    }
    for (auto& t : threads) t.join();

    std::cout << "Total processed: " << processed.load() << '\n';
}

Захват processed по ссылке плюс atomic-операции дают разделяемый между потоками счётчик, который инкрементируется без data race и виден после join всех потоков. Memory ordering relaxed достаточен, потому что нет зависимости от других переменных.

Если задача состояла в том, чтобы каждый воркер вёл свой локальный счётчик и не было shared state, то и без atomic всё работало бы, только финальное значение надо получать через future:

#include <future>

void run_workers() {
    std::vector<std::future<int>> futures;
    for (int i = 0; i < 5; ++i) {
        futures.push_back(std::async(std::launch::async, []() {
            int processed = 0;
            for (int j = 0; j < 100; ++j) ++processed;
            return processed;
        }));
    }

    int total = 0;
    for (auto& f : futures) total += f.get();
    std::cout << "Total processed: " << total << '\n';
}

mutable lambda в реальном коде нужна сравнительно редко: либо для memo-функций со внутренним состоянием, либо для функторов, которые передаются один раз и не копируются. В большинстве случаев, когда хочется поставить mutable, на самом деле нужен либо захват по ссылке плюс atomic, либо архитектура без разделяемого состояния.

Задача 4: захват переменной из range-based for

Код собирает callbacks, каждый из которых должен запомнить значение из контейнера на момент создания:

#include <functional>
#include <iostream>
#include <vector>

std::vector<std::function<void()>> create_callbacks() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    std::vector<std::function<void()>> callbacks;

    for (const auto& value : data) {
        callbacks.push_back([&value]() {
            std::cout << value << ' ';
        });
    }

    return callbacks;
}

int main() {
    auto callbacks = create_callbacks();
    for (const auto& cb : callbacks) cb();
    std::cout << '\n';
}

Программа выводит «5 5 5 5 5» (или другую последовательность одинаковых чисел, или вообще мусор) вместо ожидаемого «1 2 3 4 5». В чём подвох именно с range-based for, чего бы не случилось с обычным for-циклом по индексу?

value - это ссылочная переменная в range-based for. В каждой итерации она привязывается к следующему элементу контейнера, но это одна и та же ссылочная переменная, которая просто переуказывает. Лямбда захватывает её по ссылке через [&value], и все лямбды получают ссылку на одну и ту же переменную value.

После выхода из цикла переменная value уничтожается, и каждая лямбда содержит висячую ссылку. Дальше плюс тот факт, что create_callbacks возвращает callbacks, выходя из функции, ссылка указывает на стек-фрейм, который уже не существует. Двойное undefined behavior.

Решение очевидное, но не сразу заметное при беглом чтении: захватывать значение, а не ссылку.

for (const auto& value : data) {
    callbacks.push_back([value]() {
        std::cout << value << ' ';
    });
}

Теперь каждая лямбда копирует текущее значение value в свой собственный storage, и все пять лямбд получают разные числа. Программа выводит «1 2 3 4 5».

Особенность range-based for в том, что промежуточная переменная цикла одна на весь цикл, в отличие от языков вроде JavaScript с let-биндингом, где для каждой итерации создаётся новая переменная. В C++ это сделано из соображений производительности, но цена для лямбд высока: захват по ссылке в цикле почти всегда ошибка, кроме случаев, когда лямбда гарантированно выполняется до конца итерации.

Если очень нужен захват по ссылке (например, дорогой объект, который нельзя копировать), стоит использовать структуру shared_ptr и захватывать его по значению, или использовать capture init с C++14:

for (const auto& value : data) {
    callbacks.push_back([v = value]() {
        std::cout << v << ' ';
    });
}

Capture init копирует выражение в новую переменную внутри лямбды, что эквивалентно [value] для простых типов.

Задача 5: move-only типы в std::function

Современный код часто работает с unique_ptr, который запрещает копирование. Логично попытаться захватить такой объект в лямбде:

#include <functional>
#include <memory>
#include <iostream>

struct Resource {
    Resource() { std::cout << "Resource constructed\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void use() { std::cout << "Resource used\n"; }
};

int main() {
    auto resource = std::make_unique<Resource>();
    std::function<void()> task = [r = std::move(resource)]() {
        r->use();
    };
    task();
}

Компилятор отказывается компилировать этот код с длинным сообщением про то, что unique_ptr не копируется. Сама лямбда [r = std::move(resource)] не копируется вместе с unique_ptr, проблема возникает на этапе обёртки в std::function.

Почему std::function это не выдерживает и какой стандартный способ обойти ограничение в C++23?

std::function требует, чтобы хранящийся в нём callable был копируемым. Это требование заложено в стандарте C++11 и не убрано до сих пор: внутри std::function может потребоваться копировать обёртку, например, при копировании самой std::function. Лямбда [r = std::move(resource)] копируема только если все её захваченные типы копируемы, а unique_ptr копируемым не является.

Логика стандартного std::function устарела за прошедшие пятнадцать лет. В современном C++ move-only callbacks встречаются сплошь и рядом: capture unique_ptr, capture файлового дескриптора, capture coroutine handle.

В C++23 в стандарте появился std::move_only_function ровно для этого случая. Он работает почти как std::function, но не требует копируемости хранящегося callable:

#include <functional>
#include <memory>

int main() {
    auto resource = std::make_unique<Resource>();
    std::move_only_function<void()> task = [r = std::move(resource)]() {
        r->use();
    };
    task();
}

Этот код компилируется и работает, потому что std::move_only_function хранит callable и не требует возможности его копирования. Сам объект task move-only: его можно перемещать, но не копировать. В большинстве сценариев move-семантика как раз и нужна (передача между потоками, хранение в контейнере).

Если по каким-то причинам C++23 ещё недоступен (старая платформа, корпоративные ограничения на стандарт), стандартный обходной путь это shared_ptr вместо unique_ptr:

auto resource = std::make_shared<Resource>();
std::function<void()> task = [resource]() {
    resource->use();
};

Shared_ptr копируется без проблем, лямбда копируется, std::function счастлива. Платится за это атомарным счётчиком ссылок, что для большинства callback-сценариев несущественно. Но архитектурно правильнее перейти на std::move_only_function, если стандарт позволяет.

В C++26 (ожидается осенью 2026) появилось дополнение std::copyable_function, которое явно обозначает требование копируемости, и старый std::function постепенно переводится в категорию устаревших с точки зрения именования. Сейчас рекомендация такова: новый код пишем на std::move_only_function или std::copyable_function в зависимости от требований, std::function оставляем для legacy-совместимости.


Что забрать в работу

Все пять задач выше показывают одно и то же: внешняя простота лямбд скрывает много неявного поведения вокруг времени жизни захваченных объектов и взаимодействия с обёртками. Захват по ссылке локальной переменной даёт dangling reference при возврате из функции. Захват this не учитывает время жизни объекта. Mutable lambda копирует состояние, а не разделяет его между копиями. Захват в range-based for ссылается на одну и ту же переменную цикла. Move-only типы не пролезают через классический std::function.

Все ситуации компилируются без ошибок и часто без предупреждений. Часть ловится санитайзерами в runtime, часть только бенчмарками или анализаторами кода. Практическое правило: захватывать по значению по умолчанию, переходить на захват по ссылке только если уверены, что лямбда не переживёт область видимости захваченных переменных. Для долгоживущих лямбд (в std::function, callbacks, std::thread) использовать смарт-указатели для управления временем жизни и явный механизм отмены подписок.

Современные стандарты постепенно закрывают исторические углы: с C++17 появился захват [*this], копирующий объект целиком, с C++23 пришёл std::move_only_function для callable с unique_ptr, в C++26 фиксируется явное разделение copyable и move-only функций. Если стандарт проекта позволяет, эти средства убирают значительную часть проблем.

Хотите глубже разобраться в современном C++ не только по статьям, но и на живых примерах из практики? Приходите на открытые уроки OTUS — их ведут преподаватели-практики, которые каждый день работают с промышленной разработкой и разбирают не абстрактную теорию, а реальные инженерные ошибки.

  • 30 июня, 20:00. «RAII в C++: фундамент надёжного управления ресурсами». Записаться
    разберём, как управлять ресурсами так, чтобы код был устойчивее к утечкам, исключениям и ошибкам времени жизни.

  • 2 июля, 20:00. «Всё, что нужно знать об управлении памятью в C++». Записаться
    поговорим о памяти, владении объектами и типичных ловушках, которые приводят к undefined behavior.

  • 16 июля, 20:00. «Выразительный C++: кодируем намерения». Записаться
    обсудим, как писать код, в котором лучше видны ownership, ограничения и ожидания разработчика.

Больше открытых уроков по разработке, инфраструктуре, аналитике, ИИ и другим ИТ-направлениям — в дайджесте.