Я заметил, что параметр vector<T>::push_back
равен const T&
. Я знаю, что перераспределение может произойти, когда я вызываю push_back, но vector<T>::back
возвращает ссылку. Так безопасно ли это?
vector<string> s;
// insert something...
s.push_back(s.back());
@BoP Что это за параграф в стандарте? Я хотел бы почитать об этом, но не нашел.
Безопасно, если вы сделали s.reserve(s.size() + 1)
раньше, чтобы перераспределение гарантированно не произошло.
Это неопределенное поведение, если вы не знаете текущую емкость и, следовательно, не можете контролировать, произойдет ли перемещение и, следовательно, итератор, а также аннулирование ссылок и значений в неопределенный момент времени.
Существуют крайние случаи (например: нетривиальный, но noexcept
конструктор перемещения, конструктор копирования), где другие ограничения, такие как строгие гарантии исключений, не позволяют использовать использование после освобождения из-за особого порядка событий в реализации, но эти ограничения легко снять, например, с помощью всего лишь конструктор копирования noexcept
, который допускает в худшем случае перемещение перед повторным копированием.
s.push_back(auto(s.back()));
с другой стороны, снова безопасно.
Есть ли источник заявления UB? Я думаю, что строгая гарантия исключения требует, чтобы ссылка сохранялась до тех пор, пока не завершится копирование n+1
-го элемента, на случай, если она выдаст ошибку. Может быть, UB для noexcept
копировщиков? Хороший обходной путь, однако, устраняет любые вопросы достоверности.
@Quimby это работает на практике для большинства реализаций, поскольку строгая гарантия исключений наиболее просто реализуется с помощью alloc,insert,move_range,dealloc. Однако вы по-прежнему передаете аргумент по ссылке, который становится недействительным в середине вызова, который в конце концов является UB.
Более общая форма обходного пути: s.push_back(auto(s.back()));
— полезно в универсальном коде. Также стоит отметить, что обходной путь в конечном итоге вызывает push_back(T&&)
— поэтому для такого типа, как строка, мы не слишком пессимизируем.
24.2.2.2 Контейнеры [container.reqmts]
- Если не указано иное (явно или путем определения функции через другие функции), вызов Функция-член контейнера или передача контейнера в качестве аргумента библиотечной функции не должны делать итераторы недействительными или изменять значения объектов внутри этого контейнера.
Было бы ошибкой думать, что, поскольку нельзя делать недействительными итераторы объектов внутри контейнера, они не могут делать недействительными ссылки на объекты внутри контейнера.
Однако легко создать тип итератора, который не будет признан недействительным, пока ссылка будет:
struct iterator {
T& operator*() { return the_vector[the_index]; }
std::vector<T>& the_vector;
std::size_t the_index;
};
Ни одна вменяемая реализация этого не делает, но стандарт этого не запрещает.
Таким образом, конструкция s.push_back(s.back());
имеет неопределенное поведение.
хороший момент-> резюме: всегда проверяйте конкретные правила для контейнера и операции, с которой вы работаете, чтобы избежать неопределенного поведения.
Я не понимаю, как вы пришли к выводу UB и почему цитата актуальна?
@user17732522 user17732522 В кавычке указаны ограничения на функции-члены контейнера в отношении аннулирования. Я упомянул об этом, потому что это может быть неправильно истолковано, но вы правы в том, что это не имеет никакого отношения к аннулированию ссылок, поскольку такое поведение не определено стандартом. Значит, УБ.
@TedLyngmo «или изменить значения объектов внутри этого контейнера» подразумевает, что ссылки также не могут быть признаны недействительными, если не указано иное. Если значение объекта в контейнере не изменилось, то ссылку на него все равно можно использовать.
@user17732522 user17732522 Я не думаю, что перемещение объекта считается изменением его значения. Ты?
@TedLyngmo Предполагается, что «Перемещение» в смысле конструктора/назначения перемещения изменяет значение исходного объекта. Если вы имеете в виду «перемещение» в смысле перемещения в новое размещение посредством создания копии, то можно утверждать, что деструктор исходного объекта на самом деле не может изменить значение, но тогда было бы дефектом не включать прекращение срока службы. в цитируемой формулировке.
@user17732522 user17732522 Вы предлагаете сделать фразу «аннулировать итераторы для или» избыточной в предложении «не следует делать итераторы недействительными или изменять значения объектов». Я не думаю, что это так.
Да, это безопасно, если предположить, что вектор изначально не пуст. push_back
должен быть реализован таким образом, чтобы он работал, поскольку не существует никаких предварительных условий для push_back(const T&)
для достижения другого эффекта.
Однако важно отметить, что
s.push_back(std::move(s.back()));
не было бы безопасно. Стандартная библиотека может предполагать, что ссылочный параметр rvalue вызова библиотеки всегда является единственной ссылкой на объект, к которому она привязана. (Это не относится только к push_back
, а в общих чертах указано в спецификации библиотеки.)
Реализация стандартной библиотеки может (и делает) выполнить эту работу, сначала копируя новый элемент в новое выделение, а затем перемещая старые элементы вектора.
См., например. здесь для libc++, здесь для реализации Microsoft или здесь для реализации libstdc++.
С гарантиями исключений проблем нет. Если конструктор копирования нового элемента выдает ошибку, старое выделение остается на месте со всем исходным содержимым.
Должно быть? Я сам не могу найти это словоблудие, поэтому был бы признателен, если бы вы указали на него.
@StoryTeller-UnslanderMonica Я добавил причину. Нет никаких предварительных условий, говорящих что-то еще. Следовательно, его необходимо поддерживать.
Самая тривиальная реализация, необходимая для обеспечения строгой гарантии исключения, также подразумевает, что большинство конструкторов копирования завершились успешно, прежде чем элементы могут быть перемещены из старого буфера. Однако ошибка здесь в том, что если бы конструктор копирования также был noexcept
, то такой порядок событий вообще не был гарантирован!
Обе перегрузки объединены здесь без каких-либо предварительных условий timsong-cpp.github.io/cppwp/n4950/vector#modifiers - Я не понимаю вашего аргумента
@StoryTeller-UnslanderMonica Ситуация со ссылкой на rvalue обрабатывается в целом для всей библиотеки в eel.is/c++draft/library#res.on.arguments-1.3.
@Ext3h Ext3h Я не понимаю, как это связано с моим ответом.
@user17732522 user17732522 единственная причина, по которой текущие стандартные реализации делают это в этом порядке, связана с строгими гарантиями исключений в определенных комбинациях спецификаций исключений, поощряющими этот порядок. Ничто не запрещает иной порядок событий. Но передача ссылки на функцию, которая делает ее недействительной, в конце концов, все еще является UB, и тот факт, что она (в настоящее время) работает, является просто эмпирическим, но не подкрепленным спецификацией.
@Ext3h «Но передача ссылки на функцию, которая делает ее недействительной, в конце концов, все еще является UB»: почему это должно быть так? Функции библиотеки определяются с точки зрения предусловий, то есть допустимых начальных состояний, а затем определенных эффектов или постусловий, определяющих результирующие состояния. В начальном состоянии ссылка не становится недействительной, и если указана функция, делающая ссылки недействительными, то она должна описывать конечное состояние, а не какое-либо промежуточное состояние. Было бы даже неясно, как предварительные условия должны взаимодействовать с этими неуказанными промежуточными состояниями.
Строгая гарантия исключения гласит, что если вставка завершается неудачно (из-за исключения), функция не имеет никакого эффекта. Это означает, что вектор не может сначала перераспределиться, а затем не скопироваться.