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

推荐订阅源

F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
WordPress大学
WordPress大学
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Secure Thoughts
博客园 - 【当耐特】
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
博客园 - 司徒正美
Last Week in AI
Last Week in AI
C
Cybersecurity and Infrastructure Security Agency CISA
P
Privacy & Cybersecurity Law Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
B
Blog
The GitHub Blog
The GitHub Blog
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Spread Privacy
Spread Privacy
Martin Fowler
Martin Fowler
博客园 - 叶小钗
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Tenable Blog
S
Securelist
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
罗磊的独立博客
T
Threat Research - Cisco Blogs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
F
Full Disclosure
Cloudbric
Cloudbric
The Cloudflare Blog
Y
Y Combinator Blog
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
H
Hacker News: Front Page
腾讯CDC
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
V2EX - 技术
V2EX - 技术
GbyAI
GbyAI
TaoSecurity Blog
TaoSecurity Blog
I
Intezer
The Last Watchdog
The Last Watchdog
G
GRAHAM CLULEY
Google Online Security Blog
Google Online Security Blog
T
The Blog of Author Tim Ferriss

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 миллионов точек без потерь
Move‑семантика в C++: пять задач, в которых легко ошибиться
artem · 2026-06-23 · via Все публикации подряд на Хабре

Средний

9 мин

12

Привет, Хабр! Move‑семантика в C++ давно стала повседневной частью языка, но в код‑ревью регулярно всплывают тонкости, на которых ошибаются даже опытные разработчики: лишний std::move в return, забытый noexcept на move‑конструкторе, неработающее перемещение из const‑объекта. Все эти ошибки компилируются без предупреждений, иногда без warnings даже с флагами -Wall -Wextra, и проявляются либо медленным кодом, либо тихим undefined behavior.

Сегодня разберём пять задач на самые частые ловушки move‑семантики. Попробуйте сначала ответить сами, потом сверьтесь с разбором.

Задача 1: лишний std::move в return

Дан простой код, возвращающий вектор по значению:

std::vector<int> create_data(size_t n) {
    std::vector<int> result;
    result.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        result.push_back(static_cast<int>(i * i));
    }
    return std::move(result);
}

Вопрос: что не так с return std::move(result) и как этот код повлияет на производительность по сравнению с обычным return result?

Когда функция возвращает локальную переменную по значению, компилятор имеет право применить named return value optimization (NRVO). Это оптимизация, при которой объект сразу конструируется в памяти вызывающей стороны, без отдельного шага перемещения или копирования. С C++17 для определённых случаев copy elision стал обязательным, для NRVO он остался разрешённым, но не гарантированным.

std::move принимает аргумент по lvalue‑ссылке и возвращает rvalue‑ссылку. Когда return получает не саму локальную переменную, а результат функции std::move, NRVO становится недоступным: компилятор больше не видит возврат именованного локального объекта, он видит возврат rvalue, для которого NRVO не применяется. В лучшем случае произойдёт move‑конструирование, в худшем для типов без move‑конструктора будет копирование.

Для std::vector<int> это пара лишних указателей и атомарная операция в аллокаторе, что почти незаметно. Для типа с дорогим move‑конструктором или для типа, у которого move‑конструктор удалён, а NRVO бы сработал, разница станет ощутимой. Правило простое: возвращайте локальные объекты как есть, без std::move, доверяйте компилятору. Современные компиляторы под -Wpessimizing-move (GCC, Clang) выдают предупреждение на такой код.

Задача 2: self‑move‑assignment

Класс управляет динамическим буфером и реализует move‑assignment:

class Buffer {
public:
    Buffer(size_t size) : size_(size), data_(new char[size]) {}

    ~Buffer() { delete[] data_; }

    Buffer(Buffer&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data_;
        data_ = other.data_;
        size_ = other.size_;
        other.data_ = nullptr;
        other.size_ = 0;
        return *this;
    }

private:
    size_t size_;
    char* data_;
};

Что произойдёт при выполнении buf = std::move(buf)? И главное, как починить это без избыточных проверок при обычном использовании?

При buf = std::move(buf) параметр other ссылается на тот же объект, что и *this. Первая же строка delete[] data_ освобождает буфер. Дальше data_ = other.data_ присваивает указатель, который только что стал dangling: он указывает на освобождённую память. В завершение other.data_ = nullptr обнуляет тот же самый data_, потому что это одно и то же поле. Деструктор позже попытается удалить nullptr (это безопасно), но в промежутке между присваиванием и обнулением объект находится в состоянии с висячим указателем.

Стандарт требует, чтобы после move‑assignment объект‑приёмник находился в валидном, хотя и неопределённом состоянии. Standard library контейнеры обычно проверяют self‑move и делают раннее возвращение, но и это не строгое требование стандарта. Для собственных классов проблема решается через идиому copy‑and‑swap или через явную проверку:

Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data_;
        data_ = std::exchange(other.data_, nullptr);
        size_ = std::exchange(other.size_, 0);
    }
    return *this;
}

Проверка this != &other добавляет один branch на каждое присваивание, что в большинстве случаев предсказывается процессором правильно и стоит почти ничего. Альтернатива через swap избавляет от проверки, но меняет момент освобождения памяти, что иногда важно для долгоживущих ресурсов.

Задача 3: emplace_back и move‑only типы

Код выглядит логично:

struct Widget {
    explicit Widget(int id, std::string name) 
        : id_(id), name_(std::move(name)) {}
    int id_;
    std::string name_;
};

std::vector<Widget> widgets;
widgets.push_back(Widget{1, "alpha"});
widgets.emplace_back(2, "beta");
widgets.emplace_back(Widget{3, "gamma"});

Все три строки компилируются. Какая из них делает лишнюю работу и почему вообще в принципе существует разница между push_back(Widget{...}) и emplace_back(...) с теми же аргументами?

  • push_back(Widget{1, "alpha"}) создаёт временный Widget в стеке, затем перемещает его в вектор через move‑конструктор. Два конструктора: обычный плюс move.

  • emplace_back(2, "beta") конструирует Widget сразу внутри буфера вектора, передавая аргументы в конструктор через perfect forwarding. Один конструктор: обычный.

  • emplace_back(Widget{3, "gamma"}) создаёт временный Widget, потом emplace_back идентифицирует, что переданный аргумент это Widget&&, и использует move‑конструктор для размещения объекта в буфере. Два конструктора: обычный плюс move. Поведение совпадает с push_back(Widget{...}), разница только в синтаксисе.

Реальная экономия от emplace_back появляется только когда аргументы передаются для конструирования объекта на месте, а не когда передаётся уже сконструированный объект. Многие разработчики используют emplace_back везде из соображений «он быстрее», получая ровно тот же код, что у push_back, но с менее читаемым синтаксисом и потенциально неявными конверсиями. Используем emplace_back только когда конструируется новый объект из его аргументов, а не когда передаётся уже готовый.

Дополнительный нюанс в том, что emplace_back принимает аргументы через универсальные ссылки и не проверяет тип на этапе вызова. Конструктор может оказаться explicit, и emplace_back его вызовет, тогда как push_back от такого же аргумента откажется. Это иногда удобно, иногда приводит к незаметным неявным преобразованиям.

Задача 4: std::move на const объекте

Код выглядит подозрительно эффективным:

class StringHolder {
public:
    StringHolder(std::string s) : data_(std::move(s)) {}
    
    void process(const std::string& input) {
        data_ = std::move(input);
    }
    
private:
    std::string data_;
};

Внутри process стоит std::move(input), входной аргумент типа const std::string&. Компиляция проходит без ошибок и без предупреждений. Что в реальности произойдёт при выполнении этой строки?

Произойдёт копирование вместо ожидаемого перемещения, и компилятор не подскажет.

Разберём по типам:

input имеет тип const std::string&. std::move(input) возвращает static_cast<const std::string&&>(input), то есть rvalue‑ссылку на константный объект. Тип результата: const std::string&&.

У std::string есть два оператора присваивания, которые могли бы подойти: operator=(const string&) (copy assignment) и operator=(string&&) noexcept (move assignment).

Перегрузка operator=(string&&) принимает rvalue‑ссылку на неконстантный объект, и она не примет const std::string&&, потому что это потребовало бы отбрасывания const. Перегрузка operator=(const string&) принимает lvalue‑ссылку на const, и const std::string&& к ней приводится через стандартные правила (rvalue‑ссылка может биндиться к const lvalue reference).

В итоге выбирается copy assignment, и std::move тут работает как очень дорогой no‑op: он только меняет тип выражения, но реального move не происходит, потому что move‑конструктор и move‑assignment перемещаемого объекта требуют, чтобы источник был неконстантным.

Исправление: убрать std::move, потому что от него тут нет пользы, или изменить сигнатуру process, чтобы принимать std::string по значению или std::string&&. Если намерение было передать владение, правильная сигнатура такая:

void process(std::string input) {
    data_ = std::move(input);
}

Вызывающая сторона сама решает, копировать или перемещать в аргумент, а внутри функции input это неконстантное lvalue, для которого move работает корректно.

Задача 5: noexcept на move‑конструкторе и vector::push_back

Два класса с одинаковой логикой, но разными аннотациями:

struct Slow {
    std::string data;
    Slow() = default;
    Slow(const Slow&) = default;
    Slow(Slow&& other) : data(std::move(other.data)) {}
};

struct Fast {
    std::string data;
    Fast() = default;
    Fast(const Fast&) = default;
    Fast(Fast&& other) noexcept : data(std::move(other.data)) {}
};

std::vector<Slow> slow_vec;
std::vector<Fast> fast_vec;
for (int i = 0; i < 1'000'000; ++i) {
    slow_vec.push_back({});
    fast_vec.push_back({});
}

Бенчмарки показывают, что fast_vec заполняется заметно быстрее. Почему noexcept на move‑конструкторе влияет на производительность vector::push_back, если внутри логика идентична?

std::vector даёт strong exception guarantee: если операция выбрасывает исключение, состояние контейнера остаётся таким же, как до операции. Когда push_back исчерпывает текущую capacity, вектор аллоцирует новый буфер, копирует или перемещает элементы и освобождает старый. Если в процессе move случится исключение, восстановить исходное состояние невозможно: часть элементов уже перемещена, их данные ушли в новый буфер.

Чтобы сохранить strong exception guarantee, vector использует трюк: при reallocation он перемещает элементы только если move‑конструктор помечен noexcept. Иначе элементы копируются, что медленнее, но безопасно: при ошибке копирования старый буфер цел, можно откатиться.

Утилита std::move_if_noexcept именно это и проверяет. Для типа Slow move‑конструктор без noexcept, поэтому vector копирует. Для Fast move‑конструктор гарантированно не бросает, и vector перемещает.

Практический вывод в том, что для типов с move‑конструктором, который реально не может бросить (например, перемещение std::string, std::unique_ptr, любых типов из стандартной библиотеки с move‑операциями), помечайте move‑конструктор и move‑assignment как noexcept. Это даёт компилятору и стандартной библиотеке возможность использовать оптимальный путь. С C++11 идиоматичный move‑конструктор для класса с pointer‑based владением выглядит так:

class Resource {
public:
    Resource(Resource&& other) noexcept
        : ptr_(std::exchange(other.ptr_, nullptr)) {}
    
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete ptr_;
            ptr_ = std::exchange(other.ptr_, nullptr);
        }
        return *this;
    }
    
private:
    Data* ptr_;
};

Если в move‑операциях есть код, который теоретически может бросить (например, выделение памяти под буфер для compact‑перемещения), noexcept ставить нельзя, иначе исключение приведёт к std::terminate. В таких случаях приходится мириться с тем, что vector будет копировать при reallocation, или использовать deque/list, у которых другие гарантии и нет переаллокаций.

В итоге

Move‑семантика выглядит простой механикой, пока не сталкиваешься с тем, как она взаимодействует с copy elision, const‑квалификаторами, exception guarantees стандартных контейнеров и правилами перегрузки. Лишний std::move в return перекрывает NRVO, self‑move‑assignment без проверки делает объект невалидным, emplace_back с готовым объектом не даёт обещанной экономии, move на const‑объекте тихо превращается в копирование, отсутствие noexcept заставляет vector копировать вместо перемещения.

Все пять ситуаций компилируются без ошибок. Часть из них ловится предупреждениями компилятора (-Wpessimizing-move, -Wself-move в новых версиях GCC и Clang), часть только бенчмарками или статическими анализаторами. Самый практичный способ не наступать на все это: помечать move‑операции noexcept там, где это правда, не дублировать std::move в return‑выражениях, проверять типы параметров перед применением std::move и помнить, что emplace_back не плацебо от всех проблем.


Ошибки с move‑семантикой часто упираются не только в синтаксис, но и в более базовые вещи: владение ресурсами, время жизни объектов и работу с памятью. Чтобы лучше понимать, почему C++ ведёт себя именно так, стоит разобраться с управлением памятью на уровне объектов и типичных ошибок.

  • 2 июля, 20:00. «Всё, что нужно знать об управлении памятью в C++». Записаться

В OTUS обучение строится на живых занятиях: преподаватели‑практики разбирают реальные рабочие ситуации, отвечают на вопросы и помогают понять, как применять материал в разработке, а не просто запомнить теорию.

Ещё больше тем и дат — в полном дайджесте открытых уроков.