Представьте, что обращение к отсутствующему ключу в std::map неожиданно изменяет контейнер. Какой механизм вызвал это изменение?
У неконстантного std::map обращение через operator[] к отсутствующему ключу вставляет новую пару. Ключ создаётся, а значение типа mapped_type — инициализируется значением по умолчанию; затем оператор возвращает ссылку на это значение.
Поэтому такое обращение может увеличить размер контейнера, вызвать конструктор значения и иметь сложность O(log N). Если ключ уже существует, вставки не происходит.
Оператор operator[] появился как удобный способ работать с ассоциативным контейнером по аналогии с массивом: получить значение по ключу или создать ячейку, если её ещё нет. Такой интерфейс особенно удобен для накопления данных и построения таблиц соответствий.
Обратная сторона удобства — чтение синтаксически не отличается от операции, которая может изменить контейнер. Поэтому для проверочного поиска в современных программах обычно выбирают методы с явно выраженной семантикой: find, contains или at.
Риск возникает, когда разработчик хочет только проверить наличие ключа или прочитать значение, но использует operator[]. Отсутствующий ключ будет добавлен, а контейнер начнёт содержать записи, которые не отражают реальные данные.
Это может приводить к неверному размеру словаря, лишним затратам памяти, неожиданным побочным эффектам конструктора значения и ошибкам в логике подсчётов. Например, чтение неизвестного идентификатора в кэше способно превратить проверку кэша в его неявное заполнение.
Для неконстантного std::map выражение с operator[] сначала ищет ключ. Если ключ найден, возвращается ссылка на существующее отображаемое значение. Если ключ отсутствует, контейнер вставляет элемент с этим ключом и значением, созданным без явно переданного значения.
После обращения размер контейнера равен одному, а строка пуста. Для std::string это корректно, поскольку создаётся пустая строка. Для пользовательского типа значение должно поддерживать требуемое контейнером создание через аллокатор; на практике это обычно означает наличие подходящего конструктора по умолчанию.
Операция поиска и возможной вставки в std::map имеет сложность O(log N), поскольку контейнер обычно реализован как сбалансированное дерево поиска. При вставке итераторы и ссылки на уже существующие элементы std::map сохраняются действительными.
Если нужна только проверка, используйте contains начиная с C++20 или find в более ранних стандартах. Если нужно получить значение без вставки, применяйте at: он выбрасывает std::out_of_range при отсутствии ключа. Для явной вставки с заранее заданным значением подходят try_emplace или insert_or_assign, поскольку они лучше выражают намерение и позволяют контролировать создание объекта.
Сервис хранит настройки пользователей в std::map<int, Settings>. Код чтения настройки для неизвестного пользователя использовал operator[], из-за чего каждый несуществующий идентификатор создавал объект Settings, увеличивал размер карты и запускал тяжёлую инициализацию.
Рассматривались три варианта. operator[] был самым коротким, но скрывал изменение состояния. at явно сигнализировал об ошибке исключением, однако исключение было неудобно для обычного отрицательного результата поиска. find не изменял контейнер, но требовал отдельно обработать итератор.
Выбрали find: отсутствие настройки являлось штатной ситуацией, а создание новой записи выполнялось только в отдельной операции. В результате чтение стало без побочных эффектов, а контейнер перестал заполняться случайными идентификаторами.
Нет. У const std::map этот оператор недоступен, потому что он потенциально вставляет элемент и тем самым изменяет контейнер. Для константного контейнера используют at или find; at возвращает значение либо выбрасывает исключение, а find возвращает итератор на найденный элемент или end().
Нет. Если ключ уже есть, operator[] только возвращает ссылку на существующее отображаемое значение. Конструктор значения, связанный с неявной вставкой, вызывается только при отсутствии ключа.
Это важно для типов с дорогим конструированием: повторное обращение по существующему ключу не создаёт временный объект и не заменяет сохранённое значение.
operator[] создаёт значение по умолчанию, после чего вызывающий код обычно присваивает ему результат. try_emplace принимает аргументы конструктора значения и конструирует его непосредственно только при отсутствии ключа.
Следовательно, try_emplace позволяет избежать ненужного создания временного или промежуточного значения и не перезаписывает уже существующий элемент. Если ключ уже есть, переданные аргументы значения не используются для изменения этого элемента.