Почему оператор std::map [] создает объект, если ключ не существует?

Я почти уверен, что где-то уже видел этот вопрос (comp.lang.C++? Google, похоже, тоже его там не находит), но быстрый поиск здесь, похоже, не находит его, вот он:

Почему оператор std::map [] создает объект, если ключ не существует? Я не знаю, но для меня это кажется нелогичным, если вы сравните с большинством других операторов [] (например, std::vector), где, если вы используете его, вы должны быть уверены, что индекс существует. Мне интересно, каково обоснование для реализации этого поведения в std::map. Как я уже сказал, не будет ли более интуитивно понятным вести себя как индекс в векторе и вылетать (вполне неопределенное поведение, я думаю) при доступе с неверным ключом?

Уточнение моего вопроса после просмотра ответов:

Хорошо, до сих пор я получил много ответов, говоря, что это в основном дешево, так почему бы и нет Я полностью согласен с этим, но почему бы не использовать для этого выделенную функцию (я думаю, что в одном из комментариев говорилось, что в java нет оператора [] и функция называется put)? Я хочу сказать, почему оператор map [] не работает как вектор? Если я использую operator[] для индекса вне диапазона для вектора, я не хотел бы, чтобы он вставлял элемент, даже если это было дешево, потому что это, вероятно, означало ошибку в моем коде. Я хочу сказать, почему это не то же самое с картой. Я имею в виду, что для меня использование оператора [] на карте означало бы: я знаю, что этот ключ уже существует (по какой-то причине, я просто вставил его, у меня где-то есть избыточность, что угодно). Я думаю, что это будет более интуитивно понятно.

Тем не менее, в чем преимущество выполнения текущего поведения с оператором [] (и только для этого я согласен, что должна быть функция с текущим поведением, а не оператор [])? Может быть, так будет понятнее? Я не знаю.

Другой ответ состоял в том, что он уже существовал таким образом, так почему бы не оставить его, но тогда, возможно, когда они (те, что были до stl) решили реализовать его так, как они обнаружили, что это дает преимущество или что-то еще? Таким образом, мой вопрос в основном: почему вы решили реализовать его таким образом, имея в виду некоторое отсутствие согласованности с другим оператором []. Какую пользу это дает?

Спасибо

13 ответов

Так как operator[] возвращает ссылку на само значение, поэтому единственный способ указать на проблему состоит в том, чтобы вызвать исключение (и в общем случае STL редко генерирует исключения).

Если вам не нравится это поведение, вы можете использовать map::find вместо. Он возвращает итератор вместо значения. Это позволяет ему возвращать специальный итератор, когда значение не найдено (возвращается map::end), но также требует от разыменования итератора, чтобы получить значение.

Стандарт говорит (23.3.1.2/1), что оператор [] возвращает (*((insert(make_pair(x, T()))).first)).second, Вот в чем причина. Возвращает ссылку T&, Нет способа вернуть неверную ссылку. И это возвращает ссылку, потому что это очень удобно, я думаю, не так ли?

Чтобы ответить на ваш настоящий вопрос: нет убедительного объяснения, почему это было сделано именно так. "Просто так".

поскольку std::map является ассоциативным контейнером, нет четкого предопределенного диапазона ключей, которые должны существовать (или не существовать) на карте (в отличие от совершенно другой ситуации с std::vector). Это означает, что с std::map, вам нужно как не вставлять, так и вставлять функции поиска. Можно было перегрузить [] не вставляя способ и обеспечивают функцию для вставки. Или можно было бы сделать наоборот: перегрузить [] в качестве оператора вставки и предоставляют функцию для поиска без вставки. Итак, кто-то когда-то решил последовать последнему подходу. Это все, что нужно.

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

Это для целей назначения:


void test()
{
   std::map<std::string, int >myMap;
   myMap["hello"] = 5;
}

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

Есть также принципиальная разница в том, как карта обычно используется. С вектором обычно есть четкое разграничение между вещами, которые добавляют к вектору, и вещами, которые работают с тем, что уже есть в векторе. С картой это гораздо менее верно - гораздо чаще можно увидеть код, который манипулирует находящимся там элементом, если он есть, или добавляет новый элемент, если его там еще нет. Дизайн оператора [] для каждого отражает это.

map.insert(ключ, элемент); удостоверяется, что ключ находится в карте, но не перезаписывает существующее значение.

map.operator [key] = item; удостоверяется, что ключ находится в карте и перезаписывает любое существующее значение с элементом.

Обе эти операции достаточно важны, чтобы гарантировать единственную строку кода. Разработчики, вероятно, выбрали, какая операция была более интуитивной для оператора [], и создали вызов функции для другой.

Это позволяет вставлять новые элементы с operator[], как это:

std::map<std::string, int> m;
m["five"] = 5;

5 присваивается значение, возвращаемое m["five"], который является ссылкой на недавно созданный элемент. Если operator[] не будет вставлять новые элементы, это не может работать таким образом.

Ответ в том, что они хотели, чтобы реализация была удобной и быстрой.

Базовая реализация вектора - это массив. Таким образом, если в массиве 10 записей и вам нужна запись 5, функция T& vector::operator[](5) просто возвращает headptr+5. Если вы запрашиваете ввод 5400, он возвращает headptr+5400.

Основная реализация карты обычно представляет собой дерево. Каждый узел распределяется динамически, в отличие от вектора, который стандарт должен быть смежным. Таким образом, nodeptr+5 ничего не значит, а map["некоторая строка"] не означает rootptr+offset("некоторая строка").

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

Разница здесь в том, что карта хранит "индекс", то есть значение, хранящееся на карте (в ее базовом дереве RB), является std::pair, а не просто "проиндексированное" значение. Всегда есть map::find() это скажет вам, если существует пара с данным ключом.

Я знаю, что это старый вопрос, но, похоже, никто не ответил на него хорошо, ИМО. Пока не встречал упоминаний об этом:

Следует избегать возможности неопределенного поведения! Если есть какое -то разумное поведение, кроме UB, то я думаю, что мы должны пойти с этим.

std::vector/arrayдемонстрирует неопределенное поведение с плохим индексом, потому что на самом деле нет разумного варианта, поскольку это одна из самых быстрых и фундаментальных вещей, которые вы можете сделать в c/c++, и было бы неправильно пытаться что-либо проверить. Проверка - это что at()для.

std::*associative_container*уже проделал работу по нахождению того, куда будет помещен индексированный элемент, поэтому имеет смысл создать его там и вернуть. Это очень полезное поведение, и альтернативы ему выглядят гораздо менее чистыми, но даже если создание и вставка нового элемента — это не то, что вы хотели или бесполезно для вас, это все же гораздо лучший результат, чем неопределенное поведение.

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

Если бы моя интуиция насчет «а что, если там ничего нет» была бы всего лишь «неопределенным поведением», тогда мне было бы не хуже, поскольку я делал бы все, что мог, чтобы избежать этого, и точка.

Затем однажды я узнаю, что могу вставить элемент с помощью operator[]... жизнь стала лучше.

Рассмотрим такой вход - 3 блока, каждый блок 2 строки, первая строка - количество элементов во втором:

5
13 20 22 43 146
4
13 22 43 146
5
13 43 67 89 146

Задача: вычислить количество целых чисел, присутствующих во вторых строках всех трех блоков. (Для этого входного примера выходной сигнал должен быть равен 3, поскольку 13, 43 и 146 присутствуют во вторых строках всех трех блоков)

Посмотрите, насколько хорош этот код:

int main ()
{
    int n, curr;
    map<unsigned, unsigned char> myMap;
    for (int i = 0; i < 3; ++i)
    {
        cin >> n;
        for (int j = 0; j < n; ++j)
        {
            cin >> curr;
            myMap[curr]++;
        }

    }

    unsigned count = 0;
    for (auto it = myMap.begin(); it != myMap.end(); ++it)
    {
        if (it->second == 3)
            ++count;
    }

    cout << count <<endl;
    return 0;
}

В соответствии со стандартом operator[] возвращает ссылку на (*((insert(make_pair(key, T()))).first)).second, Вот почему я мог написать:

myMap[curr]++;

и он вставил элемент с ключом curr и инициализировал значение нулем, если ключ не присутствовал на карте. А также он увеличивал значение, несмотря на то, был ли элемент на карте или нет.

Видите как просто? Это хорошо, не правда ли? Это хороший пример того, что это действительно удобно.

Невозможно избежать создания объекта, потому что оператор [] не знает, как его использовать.

myMap["apple"] = "green";

или же

char const * cColor = myMyp["apple"];

Я предлагаю контейнер карты должен добавить функцию, такую ​​как

if( ! myMap.exist( "apple")) throw ...

это намного проще и лучше читать, чем

if( myMap.find( "apple") != myMap.end()) throw ...

Если вы хотите прочитать элемент с некоторым ключом из std::map,
но не уверены, существует ли он,
а в случае его отсутствия вы не хотите вставлять его случайно,
а хотите получить выброшено исключение,
но вы также не хотите вручную проверять map.find(key) != map.end()каждый раз, когда вы читаете элемент,

просто используйте map::at(key)(С++ 11)

https://www.cplusplus.com/reference/map/map/at/

Другие вопросы по тегам