Предположим, мы хотим создать контейнер map
, ассоциирующий int
с Widget
, и инициализировать созданное множество конкретными значениями. Все выглядит очень просто:
map
m[1]=1.50;
m[2]=3.67;
m[3]=10.5;
m[4]=45.8;
m[5]=0.0003;
Настолько просто, что легко упустить, что же здесь, собственно, происходит. А это очень плохо, потому что в действительности происходящее может заметно ухудшить быстродействие программы.
Функция operator[]
контейнеров map
никак не связана с функциями operator[]
контейнеров vector
, deque
и string
, а также со встроенным оператором []
, работающим с массивами. Функция map::operator[]
упрощает операции «обновления с возможным созданием». Иначе говоря, при наличии объявления map
команда m[k]=v;
проверяет, присутствует ли ключ k
в контейнере. Если ключ отсутствует, он добавляется вместе с ассоциированным значением v
. Если ключ уже присутствует, ассоциированное с ним значение заменяется на v
.
Для этого operator[]
возвращает ссылку на объект значения, ассоциированного с ключом k
, после чего v присваивается объекту, к которому относится эта ссылка. При обновлении значения, ассоциированного с существующим ключом, никаких затруднений не возникает — в контейнере уже имеется объект, ссылка на который возвращается функцией operator[]
. Но при отсутствии ключа k
готового объекта, на который можно было бы вернуть ссылку, не существует. В этом случае объект создается конструктором по умолчанию, после чего operator[]
возвращает ссылку на созданный объект.
Вернемся к началу исходного примера:
map
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
pair
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 при помощи оператора []