Безопасно ли использовать `vec.push_back(vec.back());`?

Я заметил, что параметр vector<T>::push_back равен const T&. Я знаю, что перераспределение может произойти, когда я вызываю push_back, но vector<T>::back возвращает ссылку. Так безопасно ли это?

vector<string> s;
// insert something...
s.push_back(s.back());

Строгая гарантия исключения гласит, что если вставка завершается неудачно (из-за исключения), функция не имеет никакого эффекта. Это означает, что вектор не может сначала перераспределиться, а затем не скопироваться.

BoP 07.09.2024 11:36

@BoP Что это за параграф в стандарте? Я хотел бы почитать об этом, но не нашел.

Ted Lyngmo 07.09.2024 13:16
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
2
80
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Безопасно, если вы сделали s.reserve(s.size() + 1) раньше, чтобы перераспределение гарантированно не произошло.

Это неопределенное поведение, если вы не знаете текущую емкость и, следовательно, не можете контролировать, произойдет ли перемещение и, следовательно, итератор, а также аннулирование ссылок и значений в неопределенный момент времени.

Существуют крайние случаи (например: нетривиальный, но noexcept конструктор перемещения, конструктор копирования), где другие ограничения, такие как строгие гарантии исключений, не позволяют использовать использование после освобождения из-за особого порядка событий в реализации, но эти ограничения легко снять, например, с помощью всего лишь конструктор копирования noexcept, который допускает в худшем случае перемещение перед повторным копированием.

s.push_back(auto(s.back())); с другой стороны, снова безопасно.

Есть ли источник заявления UB? Я думаю, что строгая гарантия исключения требует, чтобы ссылка сохранялась до тех пор, пока не завершится копирование n+1-го элемента, на случай, если она выдаст ошибку. Может быть, UB для noexcept копировщиков? Хороший обходной путь, однако, устраняет любые вопросы достоверности.

Quimby 07.09.2024 11:40

@Quimby это работает на практике для большинства реализаций, поскольку строгая гарантия исключений наиболее просто реализуется с помощью alloc,insert,move_range,dealloc. Однако вы по-прежнему передаете аргумент по ссылке, который становится недействительным в середине вызова, который в конце концов является UB.

Ext3h 07.09.2024 11:54

Более общая форма обходного пути: s.push_back(auto(s.back())); — полезно в универсальном коде. Также стоит отметить, что обходной путь в конечном итоге вызывает push_back(T&&) — поэтому для такого типа, как строка, мы не слишком пессимизируем.

StoryTeller - Unslander Monica 07.09.2024 11:59

24.2.2.2 Контейнеры [container.reqmts]

  1. Если не указано иное (явно или путем определения функции через другие функции), вызов Функция-член контейнера или передача контейнера в качестве аргумента библиотечной функции не должны делать итераторы недействительными или изменять значения объектов внутри этого контейнера.

Было бы ошибкой думать, что, поскольку нельзя делать недействительными итераторы объектов внутри контейнера, они не могут делать недействительными ссылки на объекты внутри контейнера.

Однако легко создать тип итератора, который не будет признан недействительным, пока ссылка будет:

struct iterator {
    T& operator*() { return the_vector[the_index]; }

    std::vector<T>& the_vector;
    std::size_t the_index;
};

Ни одна вменяемая реализация этого не делает, но стандарт этого не запрещает.

Таким образом, конструкция s.push_back(s.back()); имеет неопределенное поведение.

хороший момент-> резюме: всегда проверяйте конкретные правила для контейнера и операции, с которой вы работаете, чтобы избежать неопределенного поведения.

D.lola 07.09.2024 12:05

Я не понимаю, как вы пришли к выводу UB и почему цитата актуальна?

user17732522 07.09.2024 12:26

@user17732522 user17732522 В кавычке указаны ограничения на функции-члены контейнера в отношении аннулирования. Я упомянул об этом, потому что это может быть неправильно истолковано, но вы правы в том, что это не имеет никакого отношения к аннулированию ссылок, поскольку такое поведение не определено стандартом. Значит, УБ.

Ted Lyngmo 07.09.2024 12:40

@TedLyngmo «или изменить значения объектов внутри этого контейнера» подразумевает, что ссылки также не могут быть признаны недействительными, если не указано иное. Если значение объекта в контейнере не изменилось, то ссылку на него все равно можно использовать.

user17732522 07.09.2024 12:43

@user17732522 user17732522 Я не думаю, что перемещение объекта считается изменением его значения. Ты?

Ted Lyngmo 07.09.2024 12:44

@TedLyngmo Предполагается, что «Перемещение» в смысле конструктора/назначения перемещения изменяет значение исходного объекта. Если вы имеете в виду «перемещение» в смысле перемещения в новое размещение посредством создания копии, то можно утверждать, что деструктор исходного объекта на самом деле не может изменить значение, но тогда было бы дефектом не включать прекращение срока службы. в цитируемой формулировке.

user17732522 07.09.2024 12:47

@user17732522 user17732522 Вы предлагаете сделать фразу «аннулировать итераторы для или» избыточной в предложении «не следует делать итераторы недействительными или изменять значения объектов». Я не думаю, что это так.

Ted Lyngmo 07.09.2024 12:50
Ответ принят как подходящий

Да, это безопасно, если предположить, что вектор изначально не пуст. push_back должен быть реализован таким образом, чтобы он работал, поскольку не существует никаких предварительных условий для push_back(const T&) для достижения другого эффекта.

Однако важно отметить, что

s.push_back(std::move(s.back()));

не было бы безопасно. Стандартная библиотека может предполагать, что ссылочный параметр rvalue вызова библиотеки всегда является единственной ссылкой на объект, к которому она привязана. (Это не относится только к push_back, а в общих чертах указано в спецификации библиотеки.)


Реализация стандартной библиотеки может (и делает) выполнить эту работу, сначала копируя новый элемент в новое выделение, а затем перемещая старые элементы вектора.

См., например. здесь для libc++, здесь для реализации Microsoft или здесь для реализации libstdc++.

С гарантиями исключений проблем нет. Если конструктор копирования нового элемента выдает ошибку, старое выделение остается на месте со всем исходным содержимым.

Должно быть? Я сам не могу найти это словоблудие, поэтому был бы признателен, если бы вы указали на него.

StoryTeller - Unslander Monica 07.09.2024 12:01

@StoryTeller-UnslanderMonica Я добавил причину. Нет никаких предварительных условий, говорящих что-то еще. Следовательно, его необходимо поддерживать.

user17732522 07.09.2024 12:01

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

Ext3h 07.09.2024 12:03

Обе перегрузки объединены здесь без каких-либо предварительных условий timsong-cpp.github.io/cppwp/n4950/vector#modifiers - Я не понимаю вашего аргумента

StoryTeller - Unslander Monica 07.09.2024 12:04

@StoryTeller-UnslanderMonica Ситуация со ссылкой на rvalue обрабатывается в целом для всей библиотеки в eel.is/c++draft/library#res.on.arguments-1.3.

user17732522 07.09.2024 12:05

@Ext3h Ext3h Я не понимаю, как это связано с моим ответом.

user17732522 07.09.2024 12:08

@user17732522 user17732522 единственная причина, по которой текущие стандартные реализации делают это в этом порядке, связана с строгими гарантиями исключений в определенных комбинациях спецификаций исключений, поощряющими этот порядок. Ничто не запрещает иной порядок событий. Но передача ссылки на функцию, которая делает ее недействительной, в конце концов, все еще является UB, и тот факт, что она (в настоящее время) работает, является просто эмпирическим, но не подкрепленным спецификацией.

Ext3h 07.09.2024 12:52

@Ext3h «Но передача ссылки на функцию, которая делает ее недействительной, в конце концов, все еще является UB»: почему это должно быть так? Функции библиотеки определяются с точки зрения предусловий, то есть допустимых начальных состояний, а затем определенных эффектов или постусловий, определяющих результирующие состояния. В начальном состоянии ссылка не становится недействительной, и если указана функция, делающая ссылки недействительными, то она должна описывать конечное состояние, а не какое-либо промежуточное состояние. Было бы даже неясно, как предварительные условия должны взаимодействовать с этими неуказанными промежуточными состояниями.

user17732522 07.09.2024 13:14

Другие вопросы по теме