Move-семантика в C++: типичные ошибки и как их избежать
std::move не перемещает, а объект после перемещения не всегда пустой. Разбираем частые ошибки с move-семантикой в C++11–C++20 и что с ними делать.

Задача, из-за которой всё началось
Вы возвращаете большой std::vector из функции, добавляете std::move в return, чтобы «ускорить», — и получаете предупреждение компилятора -Wpessimizing-move. Или того хуже: используете объект после std::move и читаете мусор, а тесты этого не ловят. Move-семантика в C++ выглядит просто ровно до того момента, пока не начинаешь её применять по привычке, не понимая, что именно происходит с объектом.
Разберём типичные ошибки с перемещением объектов в C++11 и новее: что делает std::move на самом деле, когда компилятор перемещает сам, почему const ломает перемещение молча и что происходит с объектом после того, как его «переместили». Внутренности std::unique_ptr и написание собственных move-конструкторов — отдельная большая тема, здесь фокус только на ошибках использования.
std::move ничего не перемещает
Первое заблуждение, из которого растут остальные. std::move(x) — это приведение типа. Оно превращает lvalue в rvalue-ссылку, то есть говорит компилятору: «этот объект можно забрать». Само по себе перемещение не выполняется. Оно случится позже — когда результат попадёт в move-конструктор или move-присваивание.
#include <vector>
#include <utility>
std::vector<int> source = {1, 2, 3};
// Здесь ничего не переместилось: std::move только вернул rvalue-ссылку.
auto&& ref = std::move(source);
// source всё ещё содержит {1, 2, 3}.
// А вот здесь сработал move-конструктор vector: буфер забран у source.
std::vector<int> target = std::move(source);
// source теперь в валидном, но неопределённом состоянии.Если после std::move нет конструктора или оператора присваивания, который заберёт ресурс, — не произошло вообще ничего, кроме смены категории выражения.
std::move — это не глагол, а обещание. Оно говорит: «я больше не буду пользоваться этим объектом, забирайте». Компилятор верит вам на слово и не проверяет, сдержали ли вы его.
Ошибка 1: чтение объекта после перемещения
Стандарт гарантирует, что объект после перемещения находится в валидном, но неопределённом состоянии*. Валидное значит: деструктор отработает корректно, объект можно заново присвоить. Неопределённое значит: какое именно в нём значение — не гарантируется.
#include <string>
#include <iostream>
std::string name = "Александр";
std::string copy = std::move(name);
// Ошибка: name после перемещения может быть чем угодно.
// На практике часто пустая строка, но полагаться на это нельзя.
std::cout << name.size() << "\n"; // UB по смыслу, хотя компилятор промолчит
// Корректно: сначала присвоить новое значение.
name = "Мария";
std::cout << name.size() << "\n"; // теперь всё определеноДля std::string и std::vector из стандартной библиотеки после перемещения объект чаще всего пуст, но это не обещание стандарта, а деталь реализации. Для пользовательских типов может быть по-другому.
Ошибка 2: std::move в return
Самая частая «оптимизация», которая делает хуже. Компилятор умеет применять NRVO (named return value optimization) и, начиная с C++17, гарантированный copy elision для временных объектов. Локальную переменную в return компилятор и так вернёт перемещением или вовсе без копии.
#include <vector>
#include <utility>
// Плохо: std::move мешает NRVO. Компилятор с -Wpessimizing-move ругнётся.
std::vector<int> make_bad() {
std::vector<int> result(1000);
return std::move(result); // лишний move, блокирует оптимизацию
}
// Хорошо: компилятор сам применит NRVO или перемещение.
std::vector<int> make_good() {
std::vector<int> result(1000);
return result;
}Правило простое: не пишите std::move при возврате локальной переменной того же типа, что и возвращаемое значение. Исключение — когда возвращаете член класса, параметр по значению или объект другого типа, который приводится к возвращаемому.
Ошибка 3: const убивает перемещение молча
Move-конструктор принимает неконстантную rvalue-ссылку. Если объект const, перемещение невозможно, и компилятор тихо откатывается на копирование. Ошибки нет — есть потеря производительности, которую видно только в профайлере.
#include <string>
#include <utility>
const std::string label = "важная строка";
// Выглядит как перемещение, но это копия:
// move-конструктор string не принимает const&&.
std::string dest = std::move(label);То же касается возврата const-значения из функции: const T foo(); запрещает перемещение результата на стороне вызова. Не делайте возвращаемый тип константным по значению.
Ошибка 4: move для типов без ресурса
Перемещение выигрывает там, где есть что забрать: динамический буфер, указатель, дескриптор. Для int, double, указателя или маленькой структуры из POD-полей move-конструктор просто копирует байты — выигрыша нет, а код становится шумным.
- Есть смысл перемещать:
std::string,std::vector,std::unique_ptr, объекты с владением ресурсом. - Нет смысла: арифметические типы, сырые указатели,
std::string_view,std::span— они и так дёшевы для копирования.
Ошибка 5: перемещение из объекта, которым ещё пользуются
Классика в вызовах, где один и тот же объект используется дважды.
#include <string>
#include <utility>
void process(const std::string& a, const std::string& b);
std::string data = "полезная нагрузка";
// Опасно: порядок вычисления аргументов не гарантирован до C++17,
// а логически data "опустошён" первым использованием.
process(std::move(data), data); // так делать нельзяЕсли объект нужен после точки перемещения — не перемещайте его, копируйте. Move допустим только как последняя операция над объектом в его текущей роли.
Таблица: что происходит на самом деле
| Запись | Что происходит | Комментарий |
|---|---|---|
T b = std::move(a); | Move-конструктор | a становится неопределённым, но валидным |
T b = std::move(const_a); | Копия | const блокирует move, ошибки нет |
return local; | NRVO или move | std::move не нужен и вредит |
return std::move(member_); | Move | здесь move уместен: NRVO не применяется к члену |
auto&& r = std::move(a); | Ничего | просто rvalue-ссылка, ресурс на месте |
Как проверять код на эти ошибки
- Включите предупреждения компилятора:
-Wall -Wextra. GCC и Clang дают-Wpessimizing-moveи-Wredundant-move— они ловят лишнийstd::moveвreturn. - Прогоните код через
clang-tidyс проверками группыbugprone-*иperformance-*: они отлавливают часть проблем с перемещением. Точный список проверок сверяйте в документации вашей версии clang-tidy — набор меняется между релизами. - При ревью ищите использование переменной после
std::moveнад ней — это главный источник тихих багов. - Смотрите на
const-квалификаторы у объектов, которые вы «перемещаете»: если объект const, перемещения не будет.
Что делать дальше
Проверьте свой код на четыре вещи: лишний std::move в return, чтение объекта после перемещения, перемещение из const и перемещение объекта, который используется дальше. Эти четыре паттерна дают большую часть реальных багов и потерь производительности.
Следующий шаг — разобраться с forwarding-ссылками (T&& в шаблоне) и std::forward: там свои грабли, и std::move в шаблонном коде часто оказывается ошибкой. Правило пяти (rule of five) и написание собственных move-операций — отдельная тема, к которой стоит переходить после того, как использование std::move перестанет вызывать вопросы.
* Валидное, но неопределённое состояние — объект после перемещения корректно разрушается и может быть присвоен заново, но конкретное значение в нём стандартом не задано. Пример: std::vector обычно пуст, но полагаться на это в переносимом коде нельзя.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: std::move — cppreference.com, Copy elision — cppreference.com
Частые вопросы
Нужно ли писать std::move в return, если функция возвращает большой объект?
Можно ли использовать объект после std::move?
У меня старый компилятор с C++11, эти правила работают?
Почему перемещение const-объекта не вызывает ошибку компиляции?
Стоит ли добавлять std::move для int, double или указателей?
Как поймать эти ошибки автоматически, а не глазами?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.