возвращает ссылку на объект значения, ассоциированного с ключом
k
, после чего v присваивается объекту, к которому относится эта ссылка. При обновлении значения, ассоциированного с существующим ключом, никаких затруднений не возникает — в контейнере уже имеется объект, ссылка на который возвращается функцией
operator[]
. Но при отсутствии ключа
k
готового объекта, на который можно было бы вернуть ссылку, не существует. В этом случае объект создается конструктором по умолчанию, после чего
operator[]
возвращает ссылку на созданный объект.
Вернемся
к началу исходного примера:
map<int, Widget> m;
m[1]=1.50;
Выражение
m[1]
представляет собой сокращенную запись для
m.operator[](1)
, поэтому во второй строке присутствует вызов
map::operator[]
. Функция должна вернуть ссылку на ассоциированный объект
Widget
. В данном примере
m
еще не содержит ни одного элемента, поэтому элемент с ключом 1 не существует. Конструктор по умолчанию создает объект
Widget
, ассоциируемый с ключом 1, и возвращает ссылку на этот объект. Наконец, созданному объекту
Widget
присваивается значение 1.50.
Иначе говоря, команда
m[1]=1.50:
функционально эквивалентна следующему фрагменту:
typedef map<int, Widget> intWidgetMap; // Вспомогательное определение типа
pair<intWidgetMap::iterator, bool> result = // Создание нового
m.insert(intWidgetMap::value_type(1, Widget)); // элемента с ключом 1
// и ассоциированным объектом, созданным
// конструктором по умолчанию; комментарии
// по поводу value_type
// приведены далее
result.first->second = 1.50; // Присваивание значения
// созданному объекту
Теперь понятно, почему такой подход ухудшает быстродействие программы. Сначала мы конструируем объект
Widget
, а затем немедленно присваиваем ему новое значение. Конечно, правильнее было бы сразу сконструировать
Widget
с нужными данными вместо того, чтобы конструировать
Widget
по умолчанию и затем выполнять присваивание. Следовательно, вызов
operator[]
было бы правильнее заменить прямолинейным вызовом
insert
:
m.insert(intWidgetMap::value_type(1, 1.50));
С функциональной точки зрения эта конструкция эквивалентна фрагменту, приведенному выше, но она позволяет сэкономить три вызова функций: создание временного объекта
Widget
конструктором по умолчанию, уничтожение этого временного объекта и оператор присваивания
Widget
. Чем дороже обходятся эти вызовы, тем большую экономию обеспечивает применение
map::insert
вместо
map::operator[]
.
В приведенном выше фрагменте используется определение типа
value_type
, предоставляемое всеми стандартными контейнерами. Помните, что для
map
и
multimap
(а также для нестандартных контейнеров
hash_map
и
hash_multimap
— совет 25) тип элемента всегда представляет собой некую разновидность
pair
.
Я
уже упоминал о том, что
operator[]
упрощает операции «обновления с возможным созданием». Теперь мы знаем, что при создании insert работает эффективнее, чем
operator[]
. При обновлении, то есть при наличии эквивалентного ключа (см. совет 19) в контейнере
map
, ситуация полностью меняется. Чтобы понять, почему это происходит, рассмотрим потенциальные варианты обновления:
m[k] = v; // Значение, ассоциируемое
// с ключом k, заменяется на v при помощи оператора []
Вероятно, один внешний вид этих команд заставит вас выбрать
operator[]
, но в данном случае речь идет об эффективности, поэтому фактор наглядности не учитывается.
При вызове
insert
передается аргумент типа
inWidgetMap::value_type
(то есть
pair<int, Widget>
), потому при вызове
insert
необходимо сконструировать и уничтожить объект данного типа. Следовательно, при вызове
insert
будут вызваны конструктор и деструктор
pair
, что в свою очередь приведет к вызову конструктора и деструктора
Widget
, поскольку
pair<int, Widget>
содержит объект
Widget
. При вызове
operator[]
объект
pair
не используется, что позволяет избежать затрат на конструирование и уничтожение
pair
и
Widget
.
Следовательно, при вставке элемента в
map
по соображениям эффективности желательно использовать
insert
вместо
operator[]
, а при обновлении существующих элементов предпочтение отдается
operator[]
, что объясняется как эффективностью, так и эстетическими соображениями.
Конечно, нам хотелось бы видеть в STL функцию, которая бы автоматически выбирала оптимальное решение в синтаксически привлекательном виде. Интерфейс вызова мог бы выглядеть следующим образом:
iterator affectedPair = // Если ключ к отсутствует в контейнере m,
В STL такая функция отсутствует, но как видно из следующего фрагмента, ее нетрудно написать самостоятельно. В комментариях даются краткие пояснения, а дополнительная информация приведена после листинга.
template<typename МарТуре, // Тип контейнера
typename KeyArgType, // Причины для передачи параметров-типов
typename ValueArgType> // KeyArgType и ValueArgType