Обязательно ли, чтобы end () был постоянным в карте / наборе STL? - PullRequest
33 голосов
/ 16 сентября 2009

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

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

class Node {
    private:
        // iterators to neighboring nodes
        std::map<int, Node>::iterator neighbors[4];
        friend class Graph;
};

class Graph {
    private:
        std::map<int, Node> nodes;
};

( РЕДАКТИРОВАТЬ : Буквально не так из-за неполноты Node в строке 4 (см. Ответы / комментарии), но в любом случае по этим линиям)

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

Но допустим, вы также хотите иметь возможность хранить «недопустимое» или «несуществующее» значение соседа. Не волнуйтесь, мы можем просто использовать nodes.end() ... или мы можем? Есть ли какая-то гарантия, что nodes.end() в 8 часов утра будет таким же, как nodes.end() в 10 часов вечера после миллионного добавления / удаления? То есть можно ли безопасно == сравнить итератор, полученный в качестве параметра, с nodes.end() в некотором методе Graph?

А если нет, будет ли это работать?

class Graph {
    private:
        std::map<int, Node> nodes;
        std::map<int, Node>::iterator _INVALID;
    public:
        Graph() { _INVALID = nodes.end(); }
};

То есть я могу сохранить nodes.end() в переменной при построении, а затем использовать эту переменную всякий раз, когда я хочу установить для соседа недопустимое состояние, или сравнивать его с параметром в методе? Или же возможно, что где-нибудь ниже действительный итератор, указывающий на существующий объект, будет сравниваться равным _INVALID?

И если это тоже не сработает, что может сделать, чтобы освободить место для недопустимого значения соседа?

Ответы [ 10 ]

5 голосов
/ 16 сентября 2009

Вы пишете (выделено мной):

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

На самом деле, текст 23.1.2 / 8 немного отличается (опять же, выделение мной):

Элементы вставки не должны влиять на действительность итераторов и ссылки на контейнер , а элементы стирания должны делать недействительными только итераторы и ссылки на стертые элементы.

Я читаю это так: если у вас есть карта и каким-то образом добавлен итератор в эту карту (опять же: в нем не указано для объекта на карте), этот итератор останется действительным, несмотря на вставка и удаление элементов. Предполагая, что std::map<K,V>::end() получает «итератор в карту», ​​он не должен быть аннулирован путем вставки / удаления.

Это, конечно, оставляет вопрос, означает ли это, что «не признано недействительным», оно всегда будет иметь одинаковое значение. Мое личное предположение, что это не указано. Однако для того, чтобы фраза «не признана недействительной» имела смысл, все результаты std::map<K,V>::end() для одной и той же карты всегда должны сравниваться равными даже при вставке / удалении:

my_map_t::iterator old_end = my_map.end();
// wildly change my_map
assert( old_end == my_map.end() ); 

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

Отказ от ответственности : Я не являюсь носителем языка, и у меня очень трудное усвоение этого страшного легалаза Святого PDF. На самом деле я вообще избегаю этого, как чума.

О, и моей первой мыслью также было: вопрос интересен для академического POV, но почему он просто не хранит ключи вместо итераторов?

4 голосов
/ 16 сентября 2009

23.1 / 7 говорит, что end () возвращает итератор, который

- это значение конца контейнера для конца.

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

3 голосов
/ 16 сентября 2009

Что ж, ничто не мешает конкретной реализации коллекции иметь end(), зависящую от экземпляра коллекции и времени суток, если сравнивать и такую ​​работу. Это означает, что, возможно, значение end() может измениться, но сравнение old_end == end() все равно должно дать значение true . (редактировать: хотя после прочтения комментария от j_random_hacker, я сомневаюсь, что сам этот абзац оценивается как true ;-), не всегда - см. Обсуждение ниже)

Я также сомневаюсь, что вы можете использовать std::map<int,Node>::iterator в классе Node из-за того, что тип еще не завершен (хотя и не уверен).

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

1 голос
/ 16 сентября 2009

думаю понятно:

  • end () возвращает итератор для элемента, прошедшего через конец.

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

  • Таким образом, любой новый итератор, сгенерированный функцией end () (будет отличаться, но) по сравнению с исходным оператором ==, вернет true.

  • Также любые промежуточные значения, сгенерированные с помощью оператора присваивания =, имеют пост-условие, которое они сравнивают равным с оператором ==, а оператор == является транзитивным для итераторов.

Так что да, допустимо хранить итератор, возвращаемый end () (но только из-за гарантий с ассоциативными контейнерами, поэтому он не будет действителен для вектора и т. Д.).

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

1 голос
/ 16 сентября 2009

Итераторы в (мульти) наборах и (мульти) картах не будут аннулированы при вставках и удалениях, и, таким образом, сравнение .end () с предыдущими сохраненными значениями .end () всегда будет давать true .

Взять в качестве примера реализацию GNU libstdc ++, где .end () в картах возвращает значение по умолчанию Rb_tree_node

Из stl_tree.h:

  _M_initialize()
  {
    this->_M_header._M_color = _S_red;
    this->_M_header._M_parent = 0;
    this->_M_header._M_left = &this->_M_header;
    this->_M_header._M_right = &this->_M_header;
  }
1 голос
/ 16 сентября 2009

Пара баллов:

1) end () ссылается на элемент, который находится за концом контейнера. Он не изменяется при вставке или удалении, изменяет контейнер, поскольку не указывает на элемент.

2) Я думаю, что ваша идея хранения массива из 4 итераторов в узле может быть изменена, чтобы сделать всю проблему более понятной. Вам нужно добавить новый тип итератора к объекту Graph, который способен выполнять итерации по соседям одного узла. Реализация этого итератора должна будет получить доступ к членам карты, что, возможно, приведет вас к тому, чтобы заставить класс Graph расширить коллекцию карт. Поскольку класс Graph является расширенным std :: map, язык меняется, и вам больше не нужно хранить недопустимый итератор, вместо этого просто нужно написать алгоритм, чтобы определить, кто является «следующим соседом» на карте.

1 голос
/ 16 сентября 2009

Предполагая, что (1) карта реализована с красно-черным деревом (2) вы используете тот же экземпляр "после zillion вставок / удалений" - ответьте "Да".

Относительная реализация Я могу сказать, что все воплощенияиз STL я когда-либо знаю использовать алгоритм дерева.

0 голосов
/ 01 апреля 2011

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

Согласно стандарту, два итератора считаются эквивалентными, если оба могут быть разыменованы и &*a == &*b или если ни один из них не может быть разыменован. Поиск выделенного жирным шрифтом занял некоторое время и очень важен здесь.

Поскольку std::map::iterator не может быть признан недействительным, если только элемент, на который он указывает, не был удален, вы гарантируете, что два итератора, возвращенные end, независимо от того, какое состояние карты было при получении, всегда будут сравнить друг с другом как истинное.

0 голосов
/ 16 сентября 2009

Стандарт C ++ гласит, что итераторы должны оставаться действительными. И это. Стандарт четко гласит, что в 23.1.2 / 8:

Элементы вставки не должны влиять на достоверность итераторов и ссылки на контейнер , а члены стирания должны делать недействительными только итераторы и ссылки на стертые элементы.

А в 21,1 / 7:

end () возвращает итератор , который представляет собой значение конца 101 * * для контейнера .

Таким образом, итераторы old_end и new_end будут действительными. Это означает, что мы можем получить --old_end (назовите его it1) и --new_end (назовите его it2), и это будут итераторы конечных значений (из определения того, что возвращает end()), поскольку итератор ассоциативного контейнера относится к двунаправленному итератору категории (согласно 23.1.2 / 6) и согласно определению операции --r (таблица 75).

Теперь it1 должно быть равно it2, поскольку оно дает конечное значение, которое является только одним (23.1.2 / 9). Затем из 24.1.3 следует, что: Условие, что a == b подразумевает ++ a == ++ b . И ++it1 и ++it2 дадут old_end и new_end итераторов (из определения операции ++ r Таблица 74). Теперь мы получаем, что old_end и new_end должны быть равными .

0 голосов
/ 16 сентября 2009

Я считаю, что это полностью зависит от того, какой тип итератора используется.

В векторе end () - это тот, который находится за указателем конца, и он, очевидно, будет меняться при вставке и удалении элементов.

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

Я считаю, что итераторы set и map являются вторым видом, но я не знаю ничего, что требовало бы их реализации таким образом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...