Подтвердить что ты не робот

Почему операторы присваивания контейнеров не исключают?

Я заметил, что оператор присваивания std::string (действительно std::basic_string)) noexcept. Это имеет смысл для меня. Но затем я заметил, что ни один из стандартных контейнеров (например, std::vector, std::deque, std::list, std::map) не объявляет оператор назначения перемещения noexcept. Это имеет меньшее значение для меня. A std::vector, например, обычно реализуется как три указателя, и указатели, безусловно, могут быть назначены с переносом без исключения исключения. Затем я подумал, что, возможно, проблема связана с перемещением контейнера-распределителя, но std::string тоже имеют распределители, поэтому, если бы это было проблемой, я ожидал, что это повлияет на std::string.

Итак, почему std::string переместить оператор присваивания noexcept, но операторы присваивания пересылки для стандартных контейнеров не являются?

4b9b3361

Ответ 1

Я считаю, что мы смотрим на дефекты стандартов. Спецификация noexcept, если она применяется к оператору присваивания перемещения, несколько сложна. И я считаю, что это утверждение верно, если мы говорим о basic_string или vector.

На основе [container.requirements.general]/p7 мой английский перевод того, что должен делать оператор назначения перемещения контейнера:

C& operator=(C&& c)

Если alloc_traits::propagate_on_container_move_assignment::value true, выгружает ресурсы, перемещает назначает распределители и передает ресурсов от c.

Если alloc_traits::propagate_on_container_move_assignment::value falseи get_allocator() == c.get_allocator(), сбрасывает ресурсы и передает ресурсов от c.

Если alloc_traits::propagate_on_container_move_assignment::value falseи get_allocator() != c.get_allocator(), move присваивает каждому c[i].

Примечания:

  • alloc_traits относится к allocator_traits<allocator_type>.

  • Когда alloc_traits::propagate_on_container_move_assignment::value есть true, оператор назначения перемещения может быть указан noexcept, потому что все, что он собирается, освобождает текущие ресурсы, а затем извлекает ресурсы из источника. Также в этом случае распределитель также должен быть назначен на перенос, а назначение переноса должно быть noexcept для назначения перемещения контейнера noexcept.

  • Когда alloc_traits::propagate_on_container_move_assignment::value равно false, и если два распределителя равны, то он будет делать то же самое, что и # 2. Однако никто не знает, равны ли распределители равными до времени выполнения, поэтому вы не можете использовать noexcept для этой возможности.

  • Когда alloc_traits::propagate_on_container_move_assignment::value равно false, и если два распределителя не равны, тогда нужно переместить назначение каждого отдельного элемента. Это может включать добавление емкости или узлов к цели и, следовательно, внутренне noexcept(false).

Итак, вкратце:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment::value &&
             is_nothrow_move_assignable<allocator_type>::value);

И я не вижу зависимости от C::value_type в приведенной выше спецификации, поэтому я считаю, что он должен одинаково хорошо относиться к std::basic_string, несмотря на то, что С++ 11 указывает иначе.

Обновление

В комментариях ниже Коломбо правильно указывает, что вещи постепенно меняются все время. Мои комментарии выше относятся к С++ 11.

Для проекта С++ 17 (который кажется стабильным на данный момент) ситуация несколько изменилась:

  • Если alloc_traits::propagate_on_container_move_assignment::value - true, спецификация теперь требует назначения перемещения allocator_type, чтобы не генерировать исключения (17.6.3.5 [allocator.requirements]/p4). Поэтому больше не нужно проверять is_nothrow_move_assignable<allocator_type>::value.

  • alloc_traits::is_always_equal. Если это так, то во время компиляции можно определить, что точка 3 выше не может выбраться, потому что ресурсы могут быть переданы.

Таким образом, новая спецификация noexcept для контейнеров может быть:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment{} ||
             alloc_traits::is_always_equal{});

И, для std::allocator<T>, alloc_traits::propagate_on_container_move_assignment{} и alloc_traits::is_always_equal{} являются истинными.

Также теперь в проекте С++ 17 оба назначения vector и string переместить несут именно эту спецификацию noexcept. Однако другие контейнеры несут вариации этой спецификации noexcept.

Самая безопасная вещь, которую нужно делать, если вы беспокоитесь об этой проблеме, - это проверить явные специализации контейнеров, которые вам интересны. Я сделал именно это для container<T> для VS, libstdС++ и libС++ здесь:

http://howardhinnant.github.io/container_summary.html

Этот опрос составляет около года, но, насколько я знаю, все еще действует.

Ответ 2

Я думаю, что рассуждение для этого происходит следующим образом.

basic_string работает только с типами POD без массива. Таким образом, их деструкторы должны быть тривиальными. Это означает, что если вы выполняете swap для перемещения-назначения, для вас не имеет значения, что исходное содержимое перемещенной строки еще не было уничтожено.

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

Все существующие элементы a [перемещенный объект] либо перемещаются, либо назначаются или уничтожаются.

Таким образом, разница имеет смысл. Вы не можете сделать move-assign noexcept после того, как вы начнете освобождать память (через распределитель), потому что это может завершиться с ошибкой. Таким образом, как только вы начнете требовать освобождения памяти при назначении переадресации, вы отказываетесь от возможности применять noexcept.