Сохранение допустимости итераторов std::list посредством вставки

Примечание. Вопрос не в том, следует ли мне "использовать список или очередь". Это вопрос валидности итераторов перед лицом insert().


Это может быть простой вопрос, и я просто слишком туп, чтобы увидеть правильный способ сделать это. Я реализую (к лучшему или к худшему) буфер сетевого трафика как std::list<char> buf, и я поддерживаю свою текущую позицию чтения как итератор readpos.

Когда я добавляю данные, я делаю что-то вроде

buf.insert(buf.end(), newdata.begin(), newdata.end());

Теперь мой вопрос: как мне сохранить действующий итератор readpos? Если он указывает на середину старого buf, то все должно быть в порядке (по гарантиям итератора для std::list), но обычно я мог прочитать и обработать все данные, и у меня есть readpos == buf.end(). После вставки я хочу, чтобы readpos всегда указывал на следующий непрочитанный символ, который в случае вставки должен быть первым вставленным.

Какие-либо предложения? (Если не считать замены буфера на std::deque<char>, который кажется гораздо лучше подходящим для этой задачи, как предлагается ниже.)

Обновление: Из быстрого теста с GCC4.4 я заметил, что двухсторонняя очередь и список ведут себя по-разному по отношению к readpos = buf.end(): после вставки в конце readpos прерывается в списке, но указывает на следующий элемент в дека. Это стандартная гарантия?

(Согласно cplusplus, любой deque::insert() недействителен все итераторы. Это нехорошо. Может быть, лучше использовать счетчик, чем итератор, для отслеживания позиции в двухсторонней очереди?)


person Kerrek SB    schedule 03.06.2011    source источник
comment
С двухсторонней очередью вы всегда будете читать с начала и вставлять в конец... Нет необходимости отслеживать конкретные операции чтения. И нет, это не стандартная гарантия; в частности, если удаление из очереди перераспределяет, прежний end() почти наверняка будет ссылаться на освобожденную память.   -  person Nemo    schedule 03.06.2011
comment
Просто упомянем, что с deque вы можете сохранить числовой индекс текущей позиции вместо итератора.   -  person sbk    schedule 03.06.2011
comment
Лучше не полагаться на поведение при тестировании; он может продемонстрировать, что не работает, но не гарантирует, что все работает. Например, итератор может оставаться действительным, если хранилище не было перераспределено, но на это нельзя полагаться.   -  person Mark Ransom    schedule 03.06.2011
comment
Спасибо всем -- да, я просто буду хранить числовое значение позиции чтения. Немо: Я не в состоянии всегда читать впереди. Я предполагаю, что я мог бы немедленно удалить данные, которые я читал спереди, но есть причины, по которым я мог бы захотеть удалить только после некоторой обработки.   -  person Kerrek SB    schedule 03.06.2011
comment
Я думаю, что заголовок вашего вопроса должен быть Сохранение итераторов std::deque действительными при вставке, потому что итераторы std::list остаются действительными при любой вставке. Кроме того, если вы хотите знать, следует ли вам использовать list или deque, я советую использовать vector и целочисленные индексы (пока размер данных относительно мал). Хорошая рыба-дурманка, кстати.   -  person bobobobo    schedule 06.09.2013
comment
@bobobobo: (Спасибо.) Вопрос об итераторе end(), для которого я не смог найти стандартную гарантию того, что он останется в силе.   -  person Kerrek SB    schedule 06.09.2013


Ответы (4)


if (readpos == buf.begin())
{
    buf.insert(buf.end(), newdata.begin(), newdata.end());
    readpos = buf.begin();
}
else
{
    --readpos;
    buf.insert(buf.end(), newdata.begin(), newdata.end());
    ++readpos;
}

Не элегантно, но должно работать.

person Mark Ransom    schedule 03.06.2011
comment
Разве это не взорвется, если баф пуст?? (В настоящее время я использую хак, который по существу сводится к тому, что вы пишете, с надлежащей проверкой на пустоту.) - person Kerrek SB; 03.06.2011
comment
@Kerrek, если buf пуст, то readpos==begin() должно быть правдой. Я предполагаю, что begin() может измениться после вставки в пустой список (точно не знаю), поэтому я исправлю код для этого случая. - person Mark Ransom; 03.06.2011
comment
О, хорошая мысль! На самом деле это может быть умеренно чистый способ сделать это. - person Kerrek SB; 03.06.2011
comment
Вы говорите о вставке в std::deque правильно? Поскольку вставка в std::list не делает итераторы недействительными. - person bobobobo; 06.09.2013
comment
@bobobobo, а что будет с end? Даже если он не признан недействительным, указывает ли он по-прежнему на конец контейнера или теперь он указывает на только что вставленный элемент? С этим кодом нет никакой двусмысленности. - person Mark Ransom; 06.09.2013

Из http://www.sgi.com/tech/stl/List.html

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

Следовательно, readpos все еще должно быть действительным после вставки.

Тем не мение...

std::list< char > — очень неэффективный способ решения этой проблемы. Для каждого байта, который вы храните в std::list, требуется указатель для отслеживания байта, плюс размер структуры узла списка, обычно еще два указателя. Это как минимум 12 или 24 байта (32 или 64-битные) памяти, используемые для отслеживания одного байта данных.

std::deque< char>, вероятно, лучший контейнер для этого. Как и std::vector, он обеспечивает вставку с постоянным временем сзади, но также обеспечивает удаление с постоянным временем спереди. Наконец, например, std::vector std::deque — это контейнер с произвольным доступом, поэтому вы можете использовать смещения/индексы вместо итераторов. Эти три особенности делают его эффективным выбором.

person Geoffrey Elliott    schedule 03.06.2011
comment
Игнорируя информацию о deque, readpos оказывается НЕдопустимым, если readpos == buf.end(), поэтому после вставки readpos не указывает на первый вновь вставленный элемент. - person Kerrek SB; 03.06.2011

Я действительно был тупым. Стандарт дает нам все необходимые инструменты. В частности, требования к контейнеру последовательности 23.2.3/9 гласят:

Итератор, возвращенный из a.insert(p, i, j), указывает на копию первого элемента, вставленного в a, или p, если i == j.

Далее в описании list::insert говорится (23.3.5.4/1):

Не влияет на достоверность итераторов и ссылок.

Так что на самом деле, если pos является моим текущим итератором внутри списка, который используется, я могу сказать:

auto it = buf.insert(buf.end(), newdata.begin(), newdata.end());

if (pos == buf.end()) { pos = it; }

Диапазон новых элементов в моем списке — [it, buf.end()), а диапазон еще не обработанных элементов — [pos, buf.end()). Это работает, потому что если pos было равно buf.end() до вставки, то оно остается равным и после вставки, поскольку вставка не делает недействительными любые итераторы, даже конец.

person Kerrek SB    schedule 16.06.2014

list<char> — очень неэффективный способ хранения строки. Она наверное в 10-20 раз больше самой строки, плюс вы гоняетесь за указателем за каждым символом...

Рассматривали ли вы вместо этого использование std::dequeue<char>?

[редактировать]

Чтобы ответить на ваш фактический вопрос, добавление и удаление элементов не делает недействительными итераторы в list... Но end() все равно будет end(). Таким образом, вам нужно будет проверить это как особый случай в точке, где вы вставляете новый элемент, чтобы обновить итератор readpos.

person Nemo    schedule 03.06.2011
comment
Как я уже сказал, хорошо это или плохо, это не мой проект, и я уверен, что есть лучшие способы сделать это. Двухсторонняя очередь, вероятно, хороша - я беспокоился о том, что удаление из середины очереди будет медленным, но я считаю, что мне когда-либо нужно будет удалять диапазоны только спереди и вставлять сзади. Является ли deque эффективным для этого? В любом случае, это будет единственное изменение typedef :-) Но мой вопрос касается валидности итератора перед лицом insert()! - person Kerrek SB; 03.06.2011
comment
@Kerrek SB: deque предназначен для использования таким образом (выталкивание и толчок из крайностей), поэтому я бы сказал, что это будет ваш лучший выбор. По крайней мере лучше, чем список - person dario_ramos; 03.06.2011
comment
Да, deque чрезвычайно эффективен для вставки и удаления элементов с начала или с конца. (Определенно не в середине...) - person Nemo; 03.06.2011
comment
Что касается вашего редактирования: да, я заметил, что end() становится недействительным, но вопрос заключается в том, как получить правильный итератор для следующего элемента. - person Kerrek SB; 03.06.2011
comment
Да, теперь я вижу проблему. В отличие от одноэлементного insert, диапазон insert не возвращает итератор. (Это похоже на недосмотр, но да ладно.) Вы можете создать свою собственную версию, которая делает это, или просто придерживаться своего текущего хака. - person Nemo; 03.06.2011