Россия и Беларусь начали и продолжают войну против целого народа Украины!

Функция std::move_if_noexcept() в С++

  Дмитрий Бушуев  | 

  |

  Обновл. 15 Сен 2021  | 

 6651

Этот урок основан на уроке о спецификациях исключений и спецификаторе noexcept в С++, где мы рассматривали строгую гарантию исключений, которая гарантирует то, что если работа функции прерывается исключением, то не произойдет утечки памяти и состояние программы не будет изменено. В частности, все конструкторы должны придерживаться строгой гарантии исключений, чтобы остальная часть программы не оставалась в измененном состоянии, если создание объекта завершится неудачей.

Проблемы с исключениями при их совместном использовании с конструкторами перемещения

Рассмотрим случай, когда мы копируем какой-то объект, и копирование по какой-то причине дает сбой (например, не хватает памяти). В таком случае копируемому объекту не причиняется никакого вреда, поскольку исходный объект не нуждается в модификации для создания копии. Мы можем отбросить неудавшуюся копию и двигаться дальше. Строгая гарантия исключений сохраняется.

Теперь рассмотрим случай, когда мы вместо копирования перемещаем объект. Операция перемещения передает право собственности на используемый ресурс от источника к целевому объекту. Если операция перемещения прерывается исключением после того, как происходит передача права собственности, то наш исходный объект останется в измененном состоянии. Это не проблема, если исходный объект является временным объектом и будет в любом случае отброшен после перемещения, но для объектов, не являющихся временными, это — проблема, т.к. мы повредили исходный объект. Для соблюдения правил строгой гарантии исключений, нам нужно было бы переместить используемый ресурс обратно в исходный объект, но если перемещение не удалось в первый раз, то нет никакой гарантии, что и обратное перемещение будет успешным.

Каким образом для конструкторов перемещения можно обеспечить выполнение строгой гарантии исключений? Для этого достаточно избегать создания исключений в теле конструктора перемещения. Но конструктор перемещения может вызывать другие конструкторы, которые потенциально способны генерировать исключения. Возьмем, к примеру, конструктор перемещения для std::pair, который должен попытаться переместить каждый подобъект в исходной паре в новый объект:

Мы будем использовать два класса: MoveClass и CopyClass, из которых создадим пару, чтобы продемонстрировать проблему строгой гарантии исключений при работе с конструкторами перемещения:

Результат выполнения программы:

destroying MoveClass(empty)
my_pair.first: MoveClass(13)
destroying MoveClass(13)
Error found: abort!
my_pair.first: MoveClass(empty)
destroying MoveClass(empty)

Давайте разберем результат вывода построчно:

   Cтрока №1: Здесь мы видим, что временный объект класса MoveClass, используемый для инициализации my_pair, уничтожается сразу же после выполнения стейтмента создания экземпляра my_pair. Он пуст (empty), так как подобъект класса MoveClass в my_pair был создан из него перемещением, о чем свидетельствует следующая строка вывода.

   Строка №2: Здесь мы видим, что my_pair.first содержит объект класса MoveClass со значением 13.

   Строка №3: Мы создали moved_pair, копируя его подобъект класса CopyClass (у него нет конструктора перемещения), но эта конструкция копирования вызвала исключение, так как мы изменили логический флаг. Построение объекта moved_pair было прервано генерацией исключения, и его уже созданные члены были уничтожены. В этом случае член класса MoveClass был уничтожен, подтверждением чего является вывод строки destroying MoveClass(13).

   Строка №4: Далее мы видим сообщение Error found: abort! из функции main().

   Строка №5: Когда мы опять пытаемся вывести на экран my_pair.first, мы снова видим, что член класса MoveClass пуст (empty). Поскольку объект moved_pair был инициализирован с помощью функции std::move(), то член класса MoveClass (который имеет конструктор перемещения) был перемещен для создания moved_pair, и объект my_pair.first был обнулен.

   Строка №6: В завершении, объект my_pair уничтожается в конце функции main().

Итак, подводя итог вышеописанных результатов, можно заключить, что конструктор перемещения std::pair использовал выбрасывающий исключение конструктор копирования класса CopyClass. Этот конструктор копирования выбрасывает исключение, вызывающее прерывание создания объекта moved_pair, из-за чего объект my_pair.first постоянно повреждается. Строгая гарантия исключений не выполняется.

На помощь спешит функция std::move_if_noexcept()


Обратите внимание, что вышеописанной проблемы можно было бы избежать, если бы std::pair попытался сделать копию вместо перемещения. В таком случае объект moved_pair не удалось бы создать, но объект my_pair не был бы изменен.

Но за копирование вместо перемещения приходится расплачиваться производительностью, даже для тех объектов, которые этого и не требуют. В идеале мы бы хотели выполнить перемещение, если это возможно сделать безопасно, и копирование — в противном случае.

К счастью, в C++ есть два механизма, которые при совместном использовании позволяют нам это выполнить. Во-первых, поскольку noexcept-функции являются функциями без исключений/сбоев, то они неявно удовлетворяют критериям строгой гарантии исключений. Таким образом, noexcept-конструктор перемещения гарантированно завершит свою работу успешно.

Во-вторых, мы можем использовать функцию std::move_if_noexcept() из Стандартной библиотеки С++, чтобы определить, следует ли выполнять перемещение или копирование. Функция std::move_if_noexcept() является аналогом функции std::move() и используется таким же образом.

Если компилятор может определить, что объект, переданный в качестве аргумента для std::move_if_noexcept(), не будет выбрасывать исключение при применении конструктора перемещения (или если объект является только перемещаемым и не имеет конструктора копирования), то std::move_if_noexcept() будет работать идентично std::move() (и вернет объект, преобразованный в r-value). В противном случае, std::move_if_noexcept() вернет обычную l-value ссылку на объект.

Ключевой момент: Функция std::move_if_noexcept() вернет перемещаемый r-value объект в том случае, если объект имеет noexcept-конструктор перемещения, в противном случае он вернет копируемый l-value объект. Мы можем задействовать спецификатор noexcept в сочетании с std::move_if_noexcept() для использования семантики перемещения только тогда, когда выполняется строгая гарантия исключений (и использовать семантику копирования в противном случае).

Давайте обновим код из предыдущего примера следующим образом:

Запуск программы покажет нам следующий результат:

destroying MoveClass(empty)
my_pair.first: MoveClass(13)
destroying MoveClass(13)
Error found: abort!
my_pair.first: MoveClass(13)
destroying MoveClass(13)

Как вы можете видеть, после того, как было выброшено исключение, подобъект my_pair.first по-прежнему указывает на значение 13.

Класс CopyClass не имеет noexcept-конструктора перемещения, поэтому std::move_if_noexcept() возвращает my_pair в качестве l-value ссылки. Это приводит к тому, что moved_pair создается с помощью конструктора копирования (а не конструктора перемещения). Конструктор копирования может смело выбрасывать исключения, поскольку он не изменяет исходный объект.

Стандартная библиотека С++ часто использует std::move_if_noexcept() для оптимизации функций, которые являются noexcept. Например, std::vector::resize() будет использовать семантику перемещения, если тип элемента имеет noexcept-конструктор перемещения, и семантику копирования в противном случае. Это означает, что std::vector обычно будет работать быстрее с объектами, имеющими noexcept-конструктор перемещения (напоминание: конструкторы перемещения по умолчанию являются noexcept, если только они не вызывают функцию, которая является noexcept(false)).

Предупреждение: Если тип имеет как потенциально вызывающую исключения семантику перемещения, так и семантику удаленного копирования (конструктор копирования и оператор присваивания копированием недоступны), то std::move_if_noexcept() откажется от строгой гарантии исключений и вызовет семантику перемещения. Этот условный отказ от строгой гарантии повсеместно встречается в стандартных библиотечных контейнерных классах, поскольку они часто используют std::move_if_noexcept().

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (29 оценок, среднее: 4,86 из 5)
Загрузка...

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *