Move-семантика
Rvalue-ссылки, move-операции, forwarding, RVO и передача владения.
26 вопросов
JuniorТеорияОчень частоЧем сигнатура move-конструктора отличается от сигнатуры copy-конструктора?
Чем сигнатура move-конструктора отличается от сигнатуры copy-конструктора?
Move-конструктор принимает rvalue-ссылку, T(T&&), а copy-конструктор — const lvalue-ссылку, T(const T&). Именно параметр T&& заставляет выбирать move-конструктор для rvalue, и его обычно помечают noexcept.
Типичные ошибки
- ✗Объявлять параметр move-ctor как
const T&&, что мешает забрать ресурсы источника - ✗Забывать
noexcept, из-за чего контейнеры откатываются к копированию при реаллокации - ✗Считать, что move-ctor выбирается для lvalue без явного
std::move
Уточняющие вопросы
- →Почему move-конструктор не должен принимать параметр по
const-ссылке? - →Почему STL-контейнерам важно, помечен ли move-ctor как
noexcept?
JuniorТеорияОчень частоЧем rvalue отличается от rvalue-ссылки?
Чем rvalue отличается от rvalue-ссылки?
rvalue — категория значения, обычно временное выражение. rvalue-ссылка — тип вида T&&, способный привязываться к rvalue. Именованная переменная типа T&& внутри своей области видимости остаётся lvalue-выражением.
Типичные ошибки
- ✗Думать, что любое выражение типа T&& является rvalue
- ✗Передавать именованную T&& переменную дальше без std::move или std::forward
- ✗Путать std::move с реальным перемещением ресурса
Уточняющие вопросы
- →Почему std::move сам ничего не перемещает?
- →В каком состоянии обязан находиться объект после перемещения?
JuniorТеорияОчень частоЧем перемещение ресурса отличается от его копирования?
Чем перемещение ресурса отличается от его копирования?
Копирование дублирует владеемый ресурс, оставляя источник нетронутым и создавая два независимых объекта. Move передаёт владение существующим ресурсом приёмнику и оставляет источник в валидном, но неопределённом состоянии.
Типичные ошибки
- ✗Думать, что move всегда глубоко копирует ресурс, просто меньшим кодом
- ✗Считать, что moved-from объект уничтожен и ему нельзя присвоить новое значение
- ✗Ожидать, что move дешевле копии для тривиальных типов вроде
int
Уточняющие вопросы
- →Почему move у
intне быстрее копирования? - →Какие операции безопасны над moved-from объектом?
JuniorТеорияОчень частоЧто на самом деле делает std::move во время выполнения?
Что на самом деле делает std::move во время выполнения?
std::move ничего не делает во время выполнения — это лишь приведение аргумента к rvalue-ссылке (T&&) на этапе компиляции. Он не перемещает данные; реальный move происходит позже и только если затем выбран move-конструктор или move-присваивание.
Типичные ошибки
- ✗Считать, что
std::moveсам что-то перемещает, а не лишь разрешает move позже - ✗Вызвать
std::moveна объекте и ожидать, что он сохранит своё значение - ✗Применять
std::moveкconst-объекту, где он молча откатывается к копии
Уточняющие вопросы
- →Почему
std::moveнаconst-объекте даёт копию? - →Что приводит к выбору move-конструктора вместо copy-конструктора?
MiddleТеорияОчень частоЧто такое std::forward и чем он отличается от std::move?
Что такое std::forward и чем он отличается от std::move?
std::move безусловно приводит выражение к rvalue-ссылке. std::forward<T> условно сохраняет исходную категорию значения в шаблонах с forwarding reference.
Типичные ошибки
- ✗Использование std::forward вне шаблонов с forwarding reference
- ✗Использование std::move при возврате локальной переменной и блокировка NRVO
- ✗Проброс одного и того же аргумента несколько раз
Уточняющие вопросы
- →Что такое forwarding reference?
- →Почему return std::move(local) может быть хуже, чем return local?
JuniorТеорияЧастоКогда компилятор перемещает автоматически без std::move?
Когда компилятор перемещает автоматически без std::move?
Когда вы делаете return локальной переменной по значению, компилятор трактует эту локальную как rvalue и автоматически выбирает move-конструктор — std::move не нужен. Запись return std::move(local) вредна: она пессимизирует, блокируя copy elision (NRVO).
Типичные ошибки
- ✗Писать
return std::move(local), что блокирует NRVO и пессимизирует возврат - ✗Считать, что голый
return localкопирует, хотя он перемещает или элидирует - ✗Добавлять
std::moveприreturnпараметра по значению, где это не нужно
Уточняющие вопросы
- →В чём разница между copy elision и автоматическим move при возврате?
- →Почему
return std::move(local)медленнее, чемreturn local?
JuniorТеорияЧастоПочему std::unique_ptr только перемещаем, и как передавать владение через API?
Почему std::unique_ptr только перемещаем, и как передавать владение через API?
unique_ptr — эксклюзивное владение; copy дал бы двух владельцев и double-delete. Copy удалены, move определены. Передача: unique_ptr<T> по значению (вызывающий std::move'ит) или unique_ptr<T>&&. Для заимствования — T&/const T&. Возврат — move через RVO.
Типичные ошибки
- ✗Присваивать
unique_ptrдругому безstd::move— ошибка компиляции - ✗Передавать
unique_ptr<T>&и неожиданно мутировать в callee - ✗Использовать
unique_ptr<T>для borrow — путает семантику владения
Уточняющие вопросы
- →Чем
std::move(uniquePtr)отличается отuniquePtr.release()? - →Почему
unique_ptr— zero-overhead по сравнению с сырым указателем?
MiddleТеорияЧастоКаково состояние объекта после того, как из него сделали move?
Каково состояние объекта после того, как из него сделали move?
После move объект валиден, но неопределён — можно уничтожить, присвоить, вызвать операции без предусловий (clear(), empty()), но полагаться на значение нельзя. Стандартные типы обычно пусты по конвенции, не по гарантии.
Типичные ошибки
- ✗Читать значение moved-from и удивляться неожиданно непустому состоянию
- ✗Вызывать функции с предусловиями (
front()на возможно пустом vector) на moved-from - ✗Считать, что свой move-ctor оставит источник в полезном состоянии, не сделав это явно
Уточняющие вопросы
- →Какие инварианты должен сохранять свой move-ctor?
- →Почему
std::unique_ptrпосле move гарантированноnullptr?
MiddleТеорияЧастоКак производный класс должен вызвать move-операции базового класса?
Как производный класс должен вызвать move-операции базового класса?
Пишите Derived(Derived&& o) noexcept : Base(std::move(o)), m_(std::move(o.m_)) {}. std::move(o) обязателен — o lvalue в теле, без move выберется copy-ctor Base. В move-assign: Base::operator=(std::move(o));, затем члены.
Типичные ошибки
- ✗Забыть
std::move(other)— тихо вызовется copy-ctorBase - ✗Перемещать члены в неверном порядке (initializer list выполняется в порядке объявления)
- ✗Бросить в списке move — частично перемещённый объект
Уточняющие вопросы
- →Почему параметр
othermove-ctor — lvalue внутри тела? - →Что если у базового класса только copy-ctor, а хочется дешёвый move?
MiddleТеорияЧастоЧто такое copy elision (RVO/NRVO)? Сколько вызовов конструктора/деструктора происходит?
Что такое copy elision (RVO/NRVO)? Сколько вызовов конструктора/деструктора происходит?
Copy elision конструирует возврат прямо в хранилище вызывающего, пропуская copy/move-ctor. RVO — для временных (обязателен с C++17 для prvalue); NRVO — для именованных локальных (опционально). При обязательном RVO T x = f(); вызывает один ctor.
Типичные ошибки
- ✗Возвращать разные именованные переменные условно и ожидать NRVO — NRVO требует единственного кандидата; условия обычно его предотвращают
- ✗Добавлять
std::moveв оператор return для локальной переменной — предотвращает NRVO (компилятор не может элидировать move, только copy/construct) - ✗Думать, что copy elision применяется всегда — в debug-сборках с
-O0NRVO может быть отключен
Уточняющие вопросы
- →В чём разница между copy elision и неявным перемещением при return (C++11)?
- →Сколько вызовов конструктора происходит в
T a = T(T(T()))с C++17 RVO и без него?
MiddleТеорияЧастоЧто произойдёт, если = delete move-конструктор?
Что произойдёт, если = delete move-конструктор?
Удалённый move-ctor участвует в overload resolution (объявлен, просто удалён) и выбирается как лучшее совпадение для rvalue — становится hard compile error. T x = std::move(y); не компилируется, а не молча падает в copy-ctor.
Типичные ошибки
- ✗
=delete'ить move ради запрета move, попутно ломая и copy - ✗Не объявить move и отключить copy — тип становится нес movable
- ✗Бездумно смешивать
= deleteи= defaultсреди пяти спецчленов
Уточняющие вопросы
- →Чем «не объявлять move» отличается от
= deletemove? - →Как
= deleteвлияет на неявные преобразования?
MiddleТеорияЧастоКогда компилятор сам генерирует move-конструктор и когда подавляет его?
Когда компилятор сам генерирует move-конструктор и когда подавляет его?
Неявно генерируется только если ни один из (dtor, copy ctor/assign, move ctor/assign) не объявлен пользователем, и все члены/базовые классы перемещаемы. Объявление любой из них подавляет неявный move — класс падает в copy.
Типичные ошибки
- ✗Писать
~T() = default;считая безвредным — подавляет неявный move - ✗Определить copy-ctor вручную и удивляться, что move падает в copy
- ✗Определить только move (
= default) и получить удалённый copy автоматически
Уточняющие вопросы
- →Почему правила автогенерации спецчленов такие строгие?
- →Чем
= defaultотличается от trivial special member?
MiddleТеорияЧастоЧто произойдёт, если сделать std::move от const-объекта?
Что произойдёт, если сделать std::move от const-объекта?
std::move(x) возвращает static_cast<remove_reference_t<T>&&>(x) — для const T это const T&&. Move-ctor берёт T&& (не const), поэтому overload resolution падает на copy-ctor (const T&). Молчаливое copy, ошибки нет.
Типичные ошибки
- ✗Помечать параметры
const T&&думая, что это rvalue ref — это const-rvalue ref, почти бесполезен - ✗Возвращать
constлокальную переменную — отключает move/RVO, принуждает copy - ✗Забывать, что fallback на copy молчаливый — скрытая регрессия
Уточняющие вопросы
- →Когда
const T&&всё-таки полезен как параметр? - →Почему возврат
const Tломает и RVO, и move?
MiddleДизайнЧастоВы пишете setter, сохраняющий аргумент в член, и хотите, чтобы он работал эффективно и для lvalue, и для rvalue, не заводя двух перегрузок. Объясните идиому «pass by value, then move», к которой это приводит — как строится параметр в каждом случае, что делает тело — и компромисс, определяющий, когда она уместна.
Вы пишете setter, сохраняющий аргумент в член, и хотите, чтобы он работал эффективно и для lvalue, и для rvalue, не заводя двух перегрузок. Объясните идиому «pass by value, then move», к которой это приводит — как строится параметр в каждом случае, что делает тело — и компромисс, определяющий, когда она уместна.
Вместо перегрузок set(const T&) и set(T&&) пишите set(T x) { m_ = std::move(x); }. Параметр строится один раз: copy из lvalue, move из rvalue. Тело всегда move'ит в член. Цена — один дешёвый move; плюс — вдвое меньше перегрузок.
Типичные ошибки
- ✗Применять by-value для дорогих на move типов — каждый вызов платит лишним move
- ✗Забыть
std::moveв теле — копирование в член вместо перемещения - ✗Смешивать идиому с шаблонами, где тип параметра неизвестен
Уточняющие вопросы
- →Когда by-value даёт такой же машинный код, как и две перегрузки?
- →Чем perfect forwarding отличается для шаблонных setter'ов?
MiddleТеорияЧастоДолжен ли move assignment обрабатывать self-assignment и как?
Должен ли move assignment обрабатывать self-assignment и как?
Self-move (x = std::move(x)) редок, но возможен. Moved-from остаётся «валидным, но неопределённым», так что no-op допустим. Copy-and-swap безопасен сам; ручной перенос требует if (this != &other) или порядка release-до-acquire.
Типичные ошибки
- ✗
deleteуказателя источника до перемещения — уничтожает то, что собираетесь move'ить - ✗Не проверить self и получить уничтоженный объект при
x = std::move(x) - ✗Делать
assert(this != &other)вместо корректной обработки
Уточняющие вопросы
- →Почему copy-and-swap автоматически обрабатывает self-assignment?
- →Стоит ли self-check в copy-assign?
MiddleТеорияЧастоЧто происходит с копирующими операциями, если объявить move-конструктор?
Что происходит с копирующими операциями, если объявить move-конструктор?
Объявление move-конструктора или move-присваивания подавляет неявную генерацию copy-операций — они становятся удалёнными, если не задать явно или = default. Правило симметрично: объявление copy-операций подавляет неявный move.
Типичные ошибки
- ✗Добавить move-конструктор и ожидать, что копирование продолжит работать
- ✗Забыть noexcept у move-операций, используемых контейнерами
- ✗Писать владеющий ресурсом класс без определения всех нужных специальных методов
Уточняющие вопросы
- →Что такое Rule of Five?
- →Почему Rule of Zero предпочтительнее?
MiddleТеорияЧастоКакие типы не получают пользы от move и почему?
Какие типы не получают пользы от move и почему?
Типы, чьё представление целиком value-based без владения heap-ресурсом: int, double, маленькие POD-структуры, std::array<T, N> тривиальных T. Для них move идентичен copy, потому что красть нечего.
Типичные ошибки
- ✗Руками писать move для структуры из трёх int — бессмысленный boilerplate
- ✗Ждать от
std::moveнаintчего-то особенного — это просто каст в rvalue - ✗Оборачивать value-тип в
unique_ptrради move, когда copy уже дешёв
Уточняющие вопросы
- →Что такое 'trivially copyable' тип и как это связано с move?
- →Почему move у
std::array<int, 100>— это copy?
MiddleПроизводительностьИногдаСделает ли std::move дешёвым перемещение структуры с большим встроенным массивом?
Сделает ли std::move дешёвым перемещение структуры с большим встроенным массивом?
Нет. Встроенный массив — часть хранилища объекта, поэтому сгенерированное перемещение поэлементно move'ит/копирует каждый элемент. Move дёшев только когда объект передаёт внешний ресурс — например указатель на heap-буфер без касания данных.
Типичные ошибки
- ✗Считать, что std::move всегда означает O(1)
- ✗Использовать огромные встроенные массивы там, где std::vector лучше выражает передаваемое владение
- ✗Забывать, что std::move — это только приведение категории значения
Уточняющие вопросы
- →Как в этой ситуации отличался бы std::vector<int>?
- →Что делает defaulted move-конструктор с array-членами?
MiddleДизайнИногдаКогда вы проектируете класс, владеющий ресурсами, Rule of Zero и Rule of Five подсказывают, нужно ли писать спецчлены (деструктор, copy/move конструктор и присваивание) самому. Объясните, что предписывает каждое правило и что именно в членах класса определяет, какое из правил применяется.
Когда вы проектируете класс, владеющий ресурсами, Rule of Zero и Rule of Five подсказывают, нужно ли писать спецчлены (деструктор, copy/move конструктор и присваивание) самому. Объясните, что предписывает каждое правило и что именно в членах класса определяет, какое из правил применяется.
Rule of Zero: если все члены сами управляют ресурсами (контейнеры, smart pointers, RAII), не определяйте спецчлены. Rule of Five: если пришлось определить одну из пяти (dtor, copy ctor/assign, move ctor/assign), обдумайте все пять — ручное владение значит Rule of Five.
Типичные ошибки
- ✗Определить только деструктор, а компилятор сгенерирует copy с double-delete
- ✗
= defaultвсем пяти при не-trivial члене — компилятор сам справится; баг в другом - ✗Обернуть
unique_ptrи прописать все пять — лишнее
Уточняющие вопросы
- →Что значит
= defaultна спецчлене? - →Когда класс становится некопируемым автоматически?
MiddleТеорияИногдаКакие STL-операции используют move, а какие всё ещё копируют?
Какие STL-операции используют move, а какие всё ещё копируют?
Вставки (push_back, emplace_back, insert) перемещают из rvalue. Реаллокация vector — move только при noexcept. Алгоритмы std::move, std::rotate, std::swap — move; std::copy, std::copy_n, std::transform — copy. Range-адаптеры C++20 — views::move.
Типичные ошибки
- ✗Вызывать
std::copy(src.begin(), src.end(), dst), ожидая move — копируется - ✗Не пометить move noexcept и удивляться copy в vector
- ✗Путать алгоритм
std::move(first, last, dest)с кастомstd::move(x)
Уточняющие вопросы
- →Как
std::move_iteratorадаптирует range для move-алгоритмов? - →Почему
std::list::spliceне требует move?
SeniorТеорияИногдаКак std::move_if_noexcept выбирает между move и copy?
Как std::move_if_noexcept выбирает между move и copy?
Возвращает rvalue-ссылку, если move-ctor noexcept или у типа нет copy-ctor; иначе const lvalue-ссылку, и следующая конструкция копирует. Контейнеры применяют это при реаллокации ради strong exception guarantee.
Типичные ошибки
- ✗Использовать
std::move(x)вместоstd::move_if_noexcept(x)в реаллокации — UB при throw - ✗Считать, что
move_if_noexceptвсегда move — для не-noexcept move + копируемых типов он copy - ✗Забывать, что move-only + не-noexcept move всё равно move (copy-fallback'а нет)
Уточняющие вопросы
- →Что такое
std::move_iteratorи как это связано? - →Почему
vector::push_backне используетmoveбезусловно?
SeniorТеорияИногдаПочему move-конструктор должен быть noexcept, чтобы std::vector использовал его при реаллокации?
Почему move-конструктор должен быть noexcept, чтобы std::vector использовал его при реаллокации?
vector::push_back даёт strong exception guarantee — если move бросит на середине реаллокации, vector не откатится. Поэтому std::move_if_noexcept: noexcept move → move, иначе → copy. Не-noexcept move молча превращается в copy.
Типичные ошибки
- ✗Забыть
noexceptна не-бросающем move-ctor — регрессия производительности - ✗Пометить move
noexcept, когда он бросает — terminate - ✗Не тестировать пути move/copy отдельно, чтобы поймать регрессию
Уточняющие вопросы
- →Как реализован
std::move_if_noexcept? - →Почему strong guarantee важна для vector и не важна для list?
SeniorПроизводительностьИногдаЧто такое pessimization из-за std::move и когда move замедляет код?
Что такое pessimization из-за std::move и когда move замедляет код?
return std::move(local) блокирует NRVO/RVO copy elision, потому что std::move(local) — уже не сам локальный объект, а rvalue-выражение на него. Без elision вы платите реальный move-вызов — дешевле copy, но не ноль.
Типичные ошибки
- ✗Ставить
std::move«на всякий случай» в return — отключает RVO - ✗Оборачивать временный
T(args)вstd::move(T(args))— временный уже rvalue - ✗Возвращать
std::move(member)из getter'а и удивляться, что caller видит мусор
Уточняющие вопросы
- →Чем RVO отличается от NRVO?
- →Когда elision обязателен с C++17?
SeniorТеорияИногдаЧто такое ref-qualified методы (& / &&) и зачем они нужны?
Что такое ref-qualified методы (& / &&) и зачем они нужны?
Методы можно квалифицировать value category this: T m() & — на lvalue, T m() && — на rvalue. Полезно для разных тел — например Buffer::data() && move'ит хранилище, ведь объект умирает, а Buffer::data() const & возвращает лишь const-вид.
Типичные ошибки
- ✗Забывать, что перегрузка
&и&&требует обе — иначе одна скрывает другую - ✗Возвращать moved-out ссылку из
&&и использовать после смерти temporary - ✗Смешивать ref-квалификаторы и cv-квалификаторы в неочевидных комбинациях
Уточняющие вопросы
- →Когда C++23 deducing-this заменяет ref-qualified перегрузки?
- →Как ref-qualifier взаимодействует с overload resolution и
this?
SeniorПроизводительностьИногдаКак small-string optimisation взаимодействует с move у std::string?
Как small-string optimisation взаимодействует с move у std::string?
Если источник помещается в inline-буфер (SSO), move копирует inline-байты — heap-указателя для swap нет. «Move бесплатен» неверно для коротких строк; SSO-move по сути — copy. Длинные строки крадут heap-указатель (O(1)). Порог SSO разный (~15–23 байта).
Типичные ошибки
- ✗Считать
std::move(short_string)бесплатным — для SSO это copy - ✗Бенчмарки на одинаково коротких строках и вывод о медленности move
- ✗Забывать, что SSO порог отличается между libstdc++, libc++, MSVC STL
Уточняющие вопросы
- →Как обычно устроен SSO-буфер у
std::string? - →Почему у
string_viewнет SSO-проблем?
SeniorТеорияИногдаКогда std::vector использует перемещение вместо копирования при перевыделении памяти?
Когда std::vector использует перемещение вместо копирования при перевыделении памяти?
std::vector при реаллокации использует move только если move-ctor noexcept (std::is_nothrow_move_constructible). Иначе copy ради strong exception guarantee: бросивший на середине move не откатить, copy оставит исходный буфер целым.
Типичные ошибки
- ✗Определять конструктор перемещения, который может бросать — молча запрещает вектору использовать его при перевыделении
- ✗Не использовать
noexceptна операциях перемещения в пользовательских контейнерах или обёртках — распространяет штраф производительности - ✗Думать, что это важно только для
std::vector—std::deque, перехешированиеstd::unordered_mapследуют тому же правилу
Уточняющие вопросы
- →Как
std::vector::reserveпредотвращает перевыделение памяти и почему его следует вызывать, когда размер известен заранее? - →Что такое
std::move_if_noexceptи где оно используется в стандартной библиотеке?