Цикл for с диапазоном

Самый простой, удобный для восприятия способ пройтись по ассоциативному массиву на C++  —  цикл for с диапазоном:

#include <iostream>
#include <map>

int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};

for (const auto& pair : age_map) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
}

return 0;
}

Это чистый и понятный способ. Функцией const auto& не создается лишних копий пар ключ-значение.

Итераторы

Для большего контроля над итерационным процессом применяются итераторы:

#include <iostream>
#include <map>

int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};

for (auto it = age_map.begin(); it != age_map.end(); ++it) {
std::cout << it->first << " is " << it->second << " years old." << std::endl;
}

return 0;
}

С ними появляется больше гибкости, например возможность начать с того или иного места в массиве или пройтись в обратном порядке.

Структурированные привязки

Чтобы воспользоваться более лаконичным синтаксисом, прибегают к структурированным привязкам версии C++17 и новее:

#include <iostream>
#include <map>

int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};

for (const auto& [name, age] : age_map) {
std::cout << name << " is " << age << " years old." << std::endl;
}

return 0;
}

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

Обход с помощью std::for_each

Любители и профессионалы функционального программирования прибегают к std::for_each:

#include <iostream>
#include <map>
#include <algorithm>

int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};

std::for_each(age_map.begin(), age_map.end(), [](const auto& pair) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
});

return 0;
}

Здесь к каждому элементу ассоциативного массива применяется функция.

Изменение значений при обходе

Чтобы изменять значения во время обхода ассоциативного массива, используется неконстантная ссылка:

#include <iostream>
#include <map>

int main() {
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};

for (auto& pair : age_map) {
pair.second += 1; // Каждому значению возраста добавляется «1»
}

// Выводится обновленный массив
for (const auto& pair : age_map) {
std::cout << pair.first << " is now " << pair.second << " years old." << std::endl;
}

return 0;
}

Внимание: изменяются значения, но не ключи, поскольку ключи ассоциативного массива  —  константы.

Реальный сценарий: счетчик частотности слов

Рассмотрим обход ассоциативного массива на практике:

#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <algorithm>

std::map<std::string, int> countWordFrequency(const std::string& text) {
std::map<std::string, int> frequency;
std::istringstream iss(text);
std::string word;

while (iss >> word) {
// Преобразуется в нижний регистр
std::transform(word.begin(), word.end(), word.begin(),
[](unsigned char c){ return std::tolower(c); });
// Убирается пунктуация
word.erase(std::remove_if(word.begin(), word.end(), ::ispunct), word.end());
// Добавляется частотность
++frequency[word];
}

return frequency;
}

void printWordFrequency(const std::map<std::string, int>& frequency) {
for (const auto& [word, count] : frequency) {
std::cout << word << ": " << count << std::endl;
}
}

int main() {
std::string text = "The quick brown fox jumps over the lazy dog. "
"The dog barks, and the fox runs away.";

auto frequency = countWordFrequency(text);
printWordFrequency(frequency);

return 0;
}

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

Производительность

Итеративный обход больших ассоциативных массивов сказывается на производительности. Сравним итеративные техники:

#include <iostream>
#include <map>
#include <chrono>
#include <string>

const int MAP_SIZE = 1000000;

std::map<int, std::string> createLargeMap() {
std::map<int, std::string> large_map;
for (int i = 0; i < MAP_SIZE; ++i) {
large_map[i] = "Value" + std::to_string(i);
}
return large_map;
}

void benchmarkIteration(const std::string& method, const std::map<int, std::string>& m,
void (*iterFunc)(const std::map<int, std::string>&)) {
auto start = std::chrono::high_resolution_clock::now();
iterFunc(m);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << method << " took " << diff.count() << " seconds" << std::endl;
}

void rangeBasedForLoop(const std::map<int, std::string>& m) {
for (const auto& pair : m) {
// С «pair.first» и «pair.second» что-то делается
volatile auto x = pair.second.length();
}
}

void iteratorBasedLoop(const std::map<int, std::string>& m) {
for (auto it = m.begin(); it != m.end(); ++it) {
// С «it->first» и «it->second» что-то делается
volatile auto x = it->second.length();
}
}

void forEachLoop(const std::map<int, std::string>& m) {
std::for_each(m.begin(), m.end(), [](const auto& pair) {
// С «pair.first» и «pair.second» что-то делается
volatile auto x = pair.second.length();
});
}

int main() {
auto large_map = createLargeMap();

benchmarkIteration("Range-based for loop", large_map, rangeBasedForLoop);
benchmarkIteration("Iterator-based loop", large_map, iteratorBasedLoop);
benchmarkIteration("std::for_each", large_map, forEachLoop);

return 0;
}

В этом тесте сравнивается способы итеративного прохождения большого ассоциативного массива. Запустите тест у себя в системе и сравните, какой из них эффективнее.

Обход ассоциативного контейнера

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

#include <iostream>
#include <map>

int main() {
std::multimap<std::string, int> scores = {
{"Alice", 95},
{"Bob", 80},
{"Alice", 92},
{"Charlie", 88},
{"Bob", 85}
};

std::string current_name = "";
for (const auto& [name, score] : scores) {
if (name != current_name) {
if (!current_name.empty()) std::cout << std::endl;
std::cout << name << "'s scores: ";
current_name = name;
}
std::cout << score << " ";
}
std::cout << std::endl;

return 0;
}

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

Типичные ошибки и как их избежать

  1. Изменение ключей при обходе. Ключи ассоциативного массива неизменны, это константы. Чтобы их изменить, элемент придется удалить и вставить повторно.
  2. Инвалидация итератора из-за добавления или удаления элементов. Будьте осторожны, изменяя ассоциативный массив при его обходе.
  3. Применение оператора [] в контекстах const. Если ключа нет, этим оператором вставляется новый элемент. При доступе только для чтения используйте at() или find().
  4. Не учитывается, что ассоциативные массивы упорядочены, а стандартные упорядочены по ключам. Не нужна упорядоченность? Тогда для повышения производительности воспользуйтесь std::unordered_map.
  5. Игнорирование стоимости доступа к элементам ассоциативного массива. Доступ к ним зависит от размера массива. Нужен частый доступ по индексу? Воспользуйтесь другим контейнером.

Заключение

В C++ ассоциативный массив проходится по-разному, в зависимости от конкретных сценариев.

Понимание этих способов  —  от простого цикла for с диапазоном до гибких итераторов  —  важно для эффективного программирования на C++.

Выбирая способ итеративного прохождения, учитывайте конкретные требования своих задач. Нужно ли менять значения при обходе? Работаете ли вы с большим набором данных, где важна производительность, и в версии C++17 или новее с возможностью воспользоваться структурированными привязками?

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: How to Iterate Through a Map in C++

Предыдущая статьяОператор ?= в JavaScript
Следующая статьяСпособен ли Node.js справиться с миллионами пользователей?