Сохранение допустимого вектора:: итератор после стирания()

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

Ну, я знаю, что вызов erase() на векторе делает недействительным итераторы для элемента и всех тех, которые ему после него, и что erase() возвращает итератор в следующий допустимый итератор, но что, если стирание происходит в другом месте?

У меня следующая ситуация (упрощенная):

ПРЕДУПРЕЖДЕНИЕ: НЕ принимайте, что это весь код. То, что показано ниже, Чрезвычайно упрощено, чтобы проиллюстрировать мою проблему. Все классы и методы, показанные ниже, на самом деле намного сложнее.

class Child {
   Parent *parent;
}

class Parent {
   vector<Child*> child;
}

void Parent::erase(Child* a) {
   // find an iterator, it, that points to Child* a
   child.erase(it);
}

int Child::update() {
   if(x()) parent.erase(*this) // Sometimes it will; sometimes (most) it won't
   return y;
}

void Parent::update() {
   int i = 0;
   for(vector<A>::iterator it = child.begin(); it != child.end(); it++)
      i += (*it)->update();
}

Итак, очевидно, что он сработает после того, как он запустит (*it)->update(), если x() возвращает true, потому что когда это произойдет, Ребенок скажет родительу удалить его из вектора, недействительным итератором.

Есть ли способ исправить это иначе, чем сделать Parent::erase() передать итератор полностью обратно на Parent::update()? Это было бы проблематично, поскольку он не вызывался для каждого вызова Child::update(), и, следовательно, для этой функции понадобился бы способ вернуть итератор к себе каждый раз в другое время, а также в настоящее время возвращает другое значение. Я также предпочел бы избежать другого аналогичного способа разделения стирания процесса из цикла обновления.

Ответ 1

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

Я видел, что другие, нестандартные контейнеры облегчают это посредством "умных" итераторов, которые знают, когда их значение было стерто (и, возможно, автопереход к следующему элементу). Тем не менее, это немного больше бухгалтерского учета.

Ответ 2

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

Вы можете сделать это, изменив возвращаемое значение Child::update на нечто вроде std::pair<int, bool>, где int - это значение, а bool указывает, должен ли этот элемент быть удален.

Если вы можете сделать метод Child::update a const (это означает, что он не изменяет объект и вызывает только другие методы const), вы можете написать простой функтор, который вы можете использовать с помощью std::remove_if. Что-то вроде этого:

class update_delete {
public:
    update_delete() : sum(0) {}
    bool operator()(const Child & child) {
        std::pair<int, bool> result = child.update();
        sum += result.first;
        return result.second;
    }
private:
    int sum;
}

Если вы не можете сделать update it const, просто поменяйте элемент с помощью какого-либо элемента со спины (вам нужно будет сохранить итератор, который всегда указывает на последний элемент, доступный для подкачки). Когда вы выполняете агрегацию, просто отбросьте конец вектора (который теперь содержит все элементы, которые должны быть удалены) с помощью vector::resize. Это аналогично использованию std::remove_if, но я не уверен, что можно/допустимо использовать его с предикатом, который изменяет объекты в последовательности.

Ответ 3

Как добавить дочерние элементы в список и удалить их после обновления каждого дочернего элемента.

Фактически вы отложите удаление до первого цикла.

Ответ 4

Если вы можете связывать оба способа удаления и id от своей функции обновления, вы можете сделать это следующим образом:

int Child::update(bool& erase) {
   erase = x();
   return y;
}

void Parent::update() {
   int i = 0;

   for(vector<A>::iterator it = child.begin(); it != child.end();) {
      bool erase = false;
      i += (*it)->update(erase);

      if (erase) {
         it = child.erase(it); // erase returns next iterator
      } else {
         ++it;
      }
   }
}

Ответ 5

Я не уверен в том, что вы - дизайнерские ограничения/цели, но вы можете обнаружить необходимость удалить дочерний элемент через открытый API и просто сделать Parent::update условно удалить.

void Parent::update()
{
    int i = 0;
    for( vector<A>::iterator it = child.begin();
         it != child.end(); /* ++it done conditionally below */ )
    {
        i += (*it)->update();
        if( (*it)->should_erase() )
        {
            it = child.erase( it );
        }
        else
        {
            ++it;
        }
    }
}

bool Child::should_erase() const
{
    return x();
}

int Child::update()
{
    // Possibly do other work here.
    return y;
}

Тогда, возможно, вы можете удалить Parent::erase.

Ответ 6

Одним из оснований для решения (как говорили другие) является переход от std::vector к std:: list, так как вы можете удалить узлы из списка без отмены ссылок на другие узлы.

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

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

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

Я описываю эту настройку более подробно в следующем сообщении блога:: http://upcoder.com/12/vector-hosted-lists/

Несколько других настроек, о которых я рассказываю в этом сообщении в блоге, которые могут иметь отношение к этим требованиям, - это связанный список с векторным размещением и связанный список с пользовательским пулом памяти.

Ответ 7

попробуйте модифицированную версию ниже:

   for(vector<A>::iterator it = child.begin(); it != child.end();)
      i += (*it++)->update();

EDIT: Это, конечно, ужасно нарушено, однако это сработает, если вы можете изменить контейнер на std::list. Без этого изменения вы можете попробовать обратный итератор (если порядок не имеет значения), т.е.

   for(vector<A>::reverse_iterator it = child.rbegin(); it != child.rend();)
      i += (*it++)->update();

Ответ 8

Я думаю, проблема в том, что ваш метод "update" также делает недействительным итераторы точно так же, как это делает std::vector:: erase. Поэтому я рекомендую вам просто сделать то же самое: верните новый, действительный итератор и apdapt вызывающий цикл соответственно.