Как узнать, что `std::vector::begin()` стал недействительным?

Фрагмент кода ниже можно увидеть по адресу cppreference.

Почему std::vector::begin() недействителен, если vector вставлен в третий раз. Я думаю, что std::vector::begin() станет недействительным, когда vector будет перераспределен.

Строго ли перераспределение определено в стандарте C++? Если нет, то, похоже, std::vector::begin() следует вызывать каждый раз, когда insert() вызывается в демонстрационном коде.

#include <iostream>
#include <iterator>
#include <vector>
 
void print(int id, const std::vector<int>& container)
{
    std::cout << id << ". ";
    for (const int x : container)
        std::cout << x << ' ';
    std::cout << '\n';
}
 
int main ()
{
    std::vector<int> c1(3, 100);
    print(1, c1);
 
    auto it = c1.begin();
    it = c1.insert(it, 200);
    print(2, c1);
 
    c1.insert(it, 2, 300);  //ME: why begin() still valid?
    print(3, c1);
 
    // `it` no longer valid, get a new one:
    it = c1.begin();
 
    std::vector<int> c2(2, 400);
    c1.insert(std::next(it, 2), c2.begin(), c2.end());
    print(4, c1);
 
    int arr[] = {501, 502, 503};
    c1.insert(c1.begin(), arr, arr + std::size(arr));
    print(5, c1);
 
    c1.insert(c1.end(), {601, 602, 603});
    print(6, c1);
}

«Почему метод Begin() все еще действителен?» В этой строке кода it больше нет begin(). Его значение было изменено в строке it = c1.insert(it, 200);, поскольку это левая часть задания.

Nathan Pierson 15.04.2024 15:23
it обновляется после каждого вызова insert. Возможно, вы пропустили, что он назначен здесь `it = c1.insert(it, 200);` (и позже он больше не обновляется, потому что не будет использоваться)
463035818_is_not_an_ai 15.04.2024 15:24

Любое изменение std::vector может сделать недействительными его итераторы, поэтому удаление или добавление элемента в вектор также может сделать недействительным начало. Таким образом, правило состоит в том, чтобы предположить, что std::begin больше не действителен после вставки или удаления элемента. (Когда это действительно произойдет, зависит от фактической реализации вашего std::vector). Таким образом, формально использование вектора Begin() после вставки или удаления является неопределенным поведением.

Pepijn Kramer 15.04.2024 15:25

Он не обязательно получает новый begin(), но получает новое допустимое значение. Однако в строке c1.insert(it, 2, 300) значение it не обновляется.

Nathan Pierson 15.04.2024 15:25

@PepijnKramer использует значение итератора, сохраненное из некоторого предыдущего вызова вектора Begin() после вставки, в отличие от использования текущего значения вектора Begin().

Caleth 15.04.2024 15:51

Полезное чтение: Правила аннулирования итераторов для контейнеров C++ объединяют правила для всех стандартных контейнеров в один легко доступный документ с разбивкой по стандартным версиям.

user4581301 15.04.2024 18:32
Недействительность итератора .
Richard Critten 15.04.2024 18:41

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

Red.Wave 16.04.2024 21:24
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
8
141
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Строго ли перераспределение определено в стандарте C++?

Определено, что std::vector разрешено перераспределять, но не указано, когда. Вы просто можете вызвать reserve или resize, прежде чем добавлять новые данные. Часто алгоритм заключается в дублировании выделенной памяти, если новые данные требуют перераспределения, то есть 2,4,8,16...

Вы просто можете взглянуть на std::vector в разделе Iterator invalidation, чтобы понять, когда итераторы будут признаны недействительными.

Почему std::vector::begin() недействителен, если векотр вставлен в третий раз.

Полагаю, это придирка к формулировке, но детали имеют значение, поэтому: std::vector::begin() не является недействительным. Он всегда возвращает действительный итератор. Только когда вектор пуст, тогда std::vector::begin() == std::vector::end(). Однако ранее сохраненный итератор может стать недействительным в любой момент при вызове insert.

Строго ли перераспределение определено в стандарте C++?

Нет. Это деталь реализации. Хотя, как указано в стандарте C++ std::vector, ясно, что перераспределение должно произойти (и, конечно, спецификация была написана с учетом этого). А требования к временной сложности вставки элементов таковы, что реализация не может перераспределять их при каждой отдельной вставке. Следовательно, каждая вставка потенциально делает итераторы недействительными.

std::vector::begin() следует вызывать каждый раз при вызове метода Insert() в демонстрационном коде.

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

Ответ принят как подходящий

Стандарт C++ дает библиотеке std свободу действий в том случае, когда она делает векторы недействительными.

Он устанавливает требования, но не требует их выполнения. Эти требования означают, что возможно семейство реализаций. Фактические реализации определяли стандартный текст, а стандартный текст, в свою очередь, определял реализации.

Большинство языков, кроме C++, имеют одну реализацию. Итак, стандарт этого языка описывает, что делает реализация. В стране C++ этого не происходит, поэтому все становится более абстрактным.

Сейчас я дам описание того, как на практике поступают разработчики C++ std. Для простоты он будет содержать несколько «лжи, рассказанной детям».

std::vector<T> управляет буфером памяти, часть которого содержит объекты типа T, а также количество объектов в этом буфере.

Когда вы выполняете операцию, результирующий вектор которой помещается в этот буфер, std::vector<T> просто использует этот буфер.

Так, например, данный вектор может иметь буфер на 7 Ts и использовать только 3. Если вы добавите элемент, он перетасует элементы и ничего не перераспределит.

Однако если вы добавили 5 элементов, они больше не поместятся в ваш буфер (так как 3+5=8, что больше 7). В этот момент std::vector<T> запускает перераспределение.

Когда происходит перераспределение — когда буфер заменяется на больший — все итераторы становятся недействительными.

Если вы увеличите .reserve() до большего размера, реализации std::vector имеют тенденцию увеличивать вектор именно до того размера, который вы запрашиваете. Но если вы выполняете .push_back или .insert или подобные операции, реализации std::vector должны выполнять так называемую «амортизированную постоянную стоимость». По сути, это означает, что они должны расти в геометрической прогрессии.

Если вы начнете с вектора с буфером, в котором есть место для 1 элемента, и продолжите отталкиваться, буфер не будет расти, как «1, 2, 3, 4, 5, 6, ...». Вместо этого он растет как «1, 2, 4, 7, 11, 17, 26, ...» или «1, 2, 4, 8, 16, 32, ...». Некоторая функция, которая асимптотически ведет себя как «k^n».

Двумя типичными темпами роста являются «вдвое больше старого размера буфера» и «в 1,5 раза больше старого размера буфера»; разные реализации выбирают разные темпы роста, и стандарт C++ достаточно гибок, чтобы позволить и то, и другое. (есть преимущества более высоких или медленных темпов роста).

Из-за такой гибкости в выборе скорости роста реализации стандарт не говорит, когда итераторы становятся недействительными для данной программы. Там просто сказано, при каких условиях они могут быть признаны недействительными.

Итератор std::vector может быть признан недействительным при каждом изменении .capacity(). .reserve() гарантирует минимум .capacity(). .size() всегда меньше или равно .capacity(). Методы .insert() и .push_* могут расти .size().

Таким образом, первый уровень безопасности — не предполагать, что итераторы остаются действительными, если вы добавляете элементы в вектор. Это правило достаточно сильное.

Иногда хочется пофантазировать. Если вы хотите проявить фантазию, вам нужно во время выполнения получить .capacity(), когда вы получаете свои итераторы, и гарантировать, что ваш набор операций по добавлению элементов никогда не пройдет проверку правильности. Если это так, вы можете гарантировать, что ваши итераторы не будут признаны недействительными при добавлении элементов.

(Другой распространенный способ признания векторных итераторов недействительными — это замена, перемещение-назначение и перемещение-конструкция из или двух векторов. В случае перемещения-конструкции и замены итераторы определены для миграции в другой контейнер; в случае move-assignment, такая гарантия не предоставляется, поскольку она оставляет на усмотрение реализации принятие решения во время выполнения, должен ли вектор перемещать элементы или перемещать буферы)

Я думаю, что самая большая ложь — это «подсчет допустимых элементов»; часто это 3 указателя (начало буфера, один прошлый конец буфера, один прошлый последний действительный элемент). Счетчик равен (последний действительный) - (начало буфера), который (всегда? обычно?) фактически не сохраняется явно.

Yakk - Adam Nevraumont 15.04.2024 15:53

один прошлый конец буфера, один последний действительный элемент. Кто они такие?

John 15.04.2024 15:54

близко к концу «... и убедитесь, что ваш набор операций по добавлению элементов никогда не пройдет проверку правильности». ? никогда не превышает этот предел?

463035818_is_not_an_ai 15.04.2024 16:12

@ 463035818_is_not_an_ai Они никогда не называли «емкость» в вашем примере кода, поэтому я понятия не имею, почему вы думаете, что никогда не передавали ее.

Yakk - Adam Nevraumont 15.04.2024 16:32

@Джон Это ценности, которые я описал. Этот комментарий является попыткой предоставить детали реализации, которые явно не являются «ложью, рассказанной детям», и не являются попыткой быть легко понятыми. Я говорю о том, что (если вы действительно посмотрите на реализацию) хранится. «Ложь, рассказанная детям», должна быть понята. Если ваш вопрос «что это такое», пожалуйста, посмотрите «ложь, которую рассказывают детям».

Yakk - Adam Nevraumont 15.04.2024 16:35

извини, я не понял твоего ответа. Мой комментарий относился к вашему предложению, которое либо я совершенно неправильно понял, либо пропущено слово, либо оно неясно.

463035818_is_not_an_ai 15.04.2024 16:38

@463035818_is_not_an_ai Ах. «это» здесь является ссылкой на более раннюю тему, в данном случае .capacity(), в том же предложении. Пропущенное слово было частью «...», которое вы удалили из предложения. «... и убедитесь, что (ваш набор операций по добавлению элементов никогда не проходит .capacity()) действителен». Может мне нужно "есть"?

Yakk - Adam Nevraumont 15.04.2024 16:51

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