При расширении std::vector почему наличие noexcept у перемещающего конструктора элемента может изменить способ переноса объектов?
При перераспределении памяти std::vector предпочитает перемещать элементы, если их перемещающий конструктор гарантированно не выбрасывает исключений. Если перемещение потенциально может завершиться исключением, но доступно копирование, vector обычно выбирает копирование, чтобы сохранить сильную гарантию исключений.
Для владеющих типов это важно: безопасное noexcept-перемещение позволяет переносить владение ресурсом без копирования и без риска оставить контейнер в частично изменённом состоянии.
До появления семантики перемещения перенос элементов контейнера обычно выполнялся копированием. Для объектов, владеющих динамической памятью, это могло быть дорого или вообще невозможно без нарушения уникальности владения.
Семантика перемещения решила проблему эффективной передачи ресурсов, а спецификатор noexcept дал стандартным контейнерам возможность отличать безопасное перемещение от потенциально опасного.
При росте размера vector выделяет новый блок памяти и конструирует в нём элементы из старого блока. Только после успешного переноса старые элементы можно уничтожить и освободить прежнюю память.
Если перемещающий конструктор может выбросить исключение, часть элементов уже может оказаться перенесённой, а часть — остаться в старом блоке. Поэтому без дополнительных гарантий контейнеру сложнее сохранить прежнее состояние.
Ошибочное объявление перемещения как noexcept особенно опасно: если такой конструктор всё же выбросит исключение, будет вызван std::terminate.
Типичная стратегия vector выглядит так:
Выбор копирования во втором случае позволяет оставить старый массив нетронутым до успешного завершения всех копирований. Это поддерживает сильную гарантию исключений: при сбое операция расширения не меняет исходный vector.
Для перемещения ресурс обычно передаётся из исходного объекта в новый, а исходный объект оставляется в корректном пустом или неопределённом с точки зрения ресурса состоянии, предусмотренном контрактом типа. Деструктор исходного объекта после этого не должен освобождать уже переданный ресурс.
У std::unique_ptr перемещение предназначено именно для передачи единственного владения и является безопасным для такого использования в контейнерах. Поэтому std::vector<std::unique_ptr<T>> может эффективно перемещать элементы при перераспределении памяти.
noexcept не означает, что вся операция добавления в vector не может завершиться исключением: всё ещё могут не удаться выделение памяти, конструирование самого нового элемента или операции пользовательского типа. Он влияет прежде всего на стратегию переноса уже существующих элементов.
Помечать перемещение как noexcept следует только при фактической гарантии отсутствия исключений. Если ресурс может быть корректно передан без выбрасывания, это обычно хорошее свойство. Если же перемещение действительно может завершиться исключением, безопаснее оставить честную спецификацию и принять возможное копирование или более слабую гарантию контейнера.
В системе хранится большой vector объектов-обёрток над файловыми дескрипторами. У обёртки удалено копирование, а перемещающий конструктор освобождает прежнее владение нового объекта и передаёт дескриптор без операций, способных выбросить исключение.
Вариант с потенциально выбрасывающим перемещением плох: при перераспределении vector не может ни скопировать элементы, ни гарантированно откатить перенос. Вариант с искусственным noexcept тоже плох, если внутренний код действительно может выбросить исключение: ошибка приведёт к аварийному завершению процесса.
Практически выбирают честное невыбрасывающее перемещение, например передают дескриптор простым обменом значений и оставляют исходный объект без владения. В результате vector быстро переносит элементы, а исключения возможны только на операциях, где они действительно ожидаемы, например при выделении нового блока памяти.
Нет. Если перемещение не объявлено как невыбрасывающее, а тип поддерживает копирование, vector может выбрать копирование ради сохранения гарантии исключений. Поэтому наличие перемещающего конструктора само по себе ещё не означает, что при расширении будет использована именно семантика перемещения.
noexcept, чтобы контейнер всегда работал безопаснее?Нет. noexcept является обещанием, а не способом сделать операцию безопасной. Если операция внутри такого перемещения выбросит исключение, программа завершится через std::terminate. Спецификатор допустим только тогда, когда отсутствие исключений обеспечено реализацией перемещения и вызываемых ею операций.
В таком случае vector вынужден использовать потенциально выбрасывающее перемещение. Если оно выбросит исключение во время перераспределения, стандартная сильная гарантия для этой операции может быть утрачена; для некоторых условий эффект операции становится не полностью гарантированным стандартом. Поэтому некопируемому владеющему типу особенно важно предоставить корректное невыбрасывающее перемещение.