Что действительно позволяет отлаживать итератор STL?

Я включил отладку итератора в приложении, указав

_HAS_ITERATOR_DEBUGGING = 1

Я ожидал, что это действительно просто проверит границы вектора, но я чувствую, что это намного больше, чем это. Какие проверки и т.д. Фактически выполняются?

Dinkumware STL, кстати.

Ответ 1

Существует ряд операций с итераторами, которые приводят к неопределенному поведению, цель этого триггера - активировать проверки во время выполнения для предотвращения его возникновения (с использованием утверждений).

Вопрос

Очевидная операция - использовать недопустимый итератор, но эта недействительность может возникать по разным причинам:

  • Неинициализированный итератор
  • Итератор для элемента, который был удален
  • Итератор элемента, физическое местоположение которого изменилось (перераспределение для vector)
  • Итератор за пределами [begin, end)

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

Существует также несколько менее очевидная причина, по которой люди склонны забывать: смешивать итераторы с разными контейнерами:

std::vector<Animal> cats, dogs;

for_each(cats.begin(), dogs.end(), /**/); // obvious bug

Это относится к более общей проблеме: допустимость диапазонов, передаваемых в алгоритмы.

  • [cats.begin(), dogs.end()) недействителен (если один не является псевдонимом для другого)
  • [cats.end(), cats.begin()) недействителен (если cats пусто??)

Решение

Решение состоит в добавлении информации к итераторам, чтобы их достоверность и достоверность заданных ими диапазонов могли быть подтверждены во время выполнения, что предотвращает возникновение неопределенного поведения.

Символ _HAS_ITERATOR_DEBUGGING служит триггером этой возможности, потому что он, к сожалению, замедляет программу. Это довольно просто в теории: каждый итератор состоит из Observer контейнера, из которого он выпущен, и таким образом уведомляется о модификации.

В Dinkumware это достигается двумя дополнениями:

  • Каждый итератор несет указатель на связанный с ним контейнер
  • Каждый контейнер содержит связанный список созданных им итераторов

И это аккуратно решает наши проблемы:

  • Неинициализированный итератор не имеет родительского контейнера, большинство операций (кроме назначения и уничтожения) вызовут утверждение
  • Итератор удаленного или перемещенного элемента был уведомлен (благодаря списку) и знает о его недействительности
  • Увеличивая и уменьшая итератор, он может проверять его нахождение в пределах
  • Проверить, что 2 итератора принадлежат одному и тому же контейнеру, так же просто, как сравнить их родительские указатели
  • Проверить допустимость диапазона так же просто, как проверить, что мы достигли конца диапазона, прежде чем достигнем конца контейнера (линейная операция для тех контейнеров, которые не доступны случайным образом, следовательно, большинство из них)

Стоимость

Стоимость высока, но есть ли у правильности цена? Мы можем разбить стоимость:

  • выделение дополнительной памяти (поддерживается дополнительный список итераторов): O(NbIterators)
  • процесс уведомления о мутирующих операциях: O(NbIterators) (обратите внимание, что push_back или insert не обязательно делают недействительными все итераторы, но erase делает)
  • проверка допустимости диапазона: O( min(last-first, container.end()-first) )

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

for (iterator_t it = vec.begin();
     it != vec.end();              // Oops
     ++it)
// body

Мы знаем, что строка Oops - дурной вкус, но здесь она еще хуже: при каждом запуске цикла мы создаем новый итератор, а затем уничтожаем его, что означает выделение и освобождение узла для списка итераторов vec... Нужно ли подчеркивать стоимость выделения/освобождения памяти в узком цикле?

Конечно, for_each не столкнулся бы с такой проблемой, что является еще одним убедительным аргументом в пользу использования алгоритмов STL вместо версий с ручным кодированием.

Ответ 2

Насколько я понимаю:

_HAS_ITERATOR_DEBUGGING отобразит диалоговое окно во время выполнения, чтобы заявить о некорректном использовании итератора, включая:

1) Итераторы, используемые в контейнере после стирания элемента

2) Итераторы, используемые в векторах после функции .push() или .insert(), называются

Ответ 3

Согласно http://msdn.microsoft.com/en-us/library/aa985982%28v=VS.80%29.aspx

Стандарт С++ описывает, какие функции-члены приводят к тому, что итераторы в контейнере становятся недействительными. Два примера:

  • Удаление элемента из контейнера приводит к тому, что итераторы становятся недействительными.
  • Увеличение размера вектора (push или insert) приводит к тому, что итераторы в векторный контейнер становятся недействительными.