Фрагмент кода ниже можно увидеть по адресу 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);
}
it
обновляется после каждого вызова insert
. Возможно, вы пропустили, что он назначен здесь `it = c1.insert(it, 200);` (и позже он больше не обновляется, потому что не будет использоваться)
Любое изменение std::vector может сделать недействительными его итераторы, поэтому удаление или добавление элемента в вектор также может сделать недействительным начало. Таким образом, правило состоит в том, чтобы предположить, что std::begin больше не действителен после вставки или удаления элемента. (Когда это действительно произойдет, зависит от фактической реализации вашего std::vector). Таким образом, формально использование вектора Begin() после вставки или удаления является неопределенным поведением.
Он не обязательно получает новый begin()
, но получает новое допустимое значение. Однако в строке c1.insert(it, 2, 300)
значение it
не обновляется.
@PepijnKramer использует значение итератора, сохраненное из некоторого предыдущего вызова вектора Begin() после вставки, в отличие от использования текущего значения вектора Begin().
Полезное чтение: Правила аннулирования итераторов для контейнеров C++ объединяют правила для всех стандартных контейнеров в один легко доступный документ с разбивкой по стандартным версиям.
В примере показано, что нельзя сохранять итераторы для последующего использования. Итераторы должны использоваться мгновенно после создания. Таким образом, их единственные допустимые варианты использования — это аргумент функции и возврат.
Строго ли перераспределение определено в стандарте 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 T
s и использовать только 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 указателя (начало буфера, один прошлый конец буфера, один прошлый последний действительный элемент). Счетчик равен (последний действительный) - (начало буфера), который (всегда? обычно?) фактически не сохраняется явно.
один прошлый конец буфера, один последний действительный элемент. Кто они такие?
близко к концу «... и убедитесь, что ваш набор операций по добавлению элементов никогда не пройдет проверку правильности». ? никогда не превышает этот предел?
@ 463035818_is_not_an_ai Они никогда не называли «емкость» в вашем примере кода, поэтому я понятия не имею, почему вы думаете, что никогда не передавали ее.
@Джон Это ценности, которые я описал. Этот комментарий является попыткой предоставить детали реализации, которые явно не являются «ложью, рассказанной детям», и не являются попыткой быть легко понятыми. Я говорю о том, что (если вы действительно посмотрите на реализацию) хранится. «Ложь, рассказанная детям», должна быть понята. Если ваш вопрос «что это такое», пожалуйста, посмотрите «ложь, которую рассказывают детям».
извини, я не понял твоего ответа. Мой комментарий относился к вашему предложению, которое либо я совершенно неправильно понял, либо пропущено слово, либо оно неясно.
@463035818_is_not_an_ai Ах. «это» здесь является ссылкой на более раннюю тему, в данном случае .capacity()
, в том же предложении. Пропущенное слово было частью «...», которое вы удалили из предложения. «... и убедитесь, что (ваш набор операций по добавлению элементов никогда не проходит .capacity()
) действителен». Может мне нужно "есть"?
«Почему метод Begin() все еще действителен?» В этой строке кода
it
больше нетbegin()
. Его значение было изменено в строкеit = c1.insert(it, 200);
, поскольку это левая часть задания.