for-each (Range-based for)for: де ховаються помилки?Уявіть собі, що вам потрібно знайти максимальний бал серед семи студентів. Ви відкриваєте редактор і пишете перше, що спадає на думку — класичний цикл for:
#include <iostream>
int main()
{
const int NUM_STUDENTS = 7;
int scores[NUM_STUDENTS] = { 45, 87, 55, 68, 80, 90, 58 };
int maxScore = 0;
for (int i = 0; i < NUM_STUDENTS; ++i)
{
if (scores[i] > maxScore)
maxScore = scores[i];
}
std::cout << "Найвищий бал: " << maxScore << '\n';
return 0;
}
Код працює. Але розгляньмо його уважніше: рядок i < NUM_STUDENTS — де ховається пастка? Якщо написати i <= NUM_STUDENTS, програма вийде за межі масиву і поведінка буде невизначеною. Якщо масив передається як параметр функції, він перетворюється на вказівник, і NUM_STUDENTS стає недоступним — виникає окрема проблема. Щоразу, пишучи такий цикл, ми змушені тримати в голові кілька деталей одночасно: початкове значення, умову зупинки, крок, правильне ім'я масиву.
Ця сукупність дрібниць породжує цілий клас помилок, відомий як «off-by-one errors» (помилки неврахованої одиниці). Саме щоб усунути цю проблему, у стандарті C++11 з'явився новий синтаксис — цикл for-each, відомий також як range-based for (цикл діапазону).
for-each, яке ми розглянемо нижче.Загальна форма циклу for-each виглядає так:
for (оголошення_елемента : діапазон)
тіло_циклу;
де:
оголошення_елемента — змінна, якій на кожній ітерації присвоюється значення чергового елемента діапазону.діапазон — масив, std::vector, std::string чи інший об'єкт, що підтримує ітерацію.Перепишемо приклад пошуку максимального балу з використанням for-each:
#include <iostream>
int main()
{
const int NUM_STUDENTS = 7;
int scores[NUM_STUDENTS] = { 45, 87, 55, 68, 80, 90, 58 };
int maxScore = 0;
for (int score : scores) // на кожній ітерації score = черговий елемент scores
{
if (score > maxScore)
maxScore = score;
}
std::cout << "Найвищий бал: " << maxScore << '\n';
return 0;
}
Розбір ключового рядка (рядок 10):
for (int score : scores) читається буквально: «для кожного score з scores». На першій ітерації score отримує значення 45, на другій — 87, на третій — 55 і так далі, доки всі елементи масиву не будуть опрацьовані. Жодного індексу, жодної умови зупинки, жодної можливості вийти за межі масиву.
auto у for-each: дозвольте компілятору думати за васУ прикладі вище ми явно написали int score. Але що, якщо тип елементів складний, або ми просто не хочемо дублювати інформацію? Ключове слово auto ідеально доповнює for-each — воно автоматично виводить тип елементу з типу масиву:
#include <iostream>
int main()
{
double temperatures[] = { 22.5, 19.0, 25.7, 18.3, 21.1 };
for (auto temp : temperatures) // компілятор виводить тип: double
{
std::cout << temp << ' ';
}
std::cout << '\n';
return 0;
}
auto у for-each — це більше, ніж зручність. Припустімо, що ви вирішите змінити тип масиву з double на float. З явним float temp вам потрібно шукати і змінювати всі такі місця у коді. З auto — достатньо змінити тип масиву, і цикл автоматично адаптується.
auto в for-each як стандартну практику. Це не «лінь», а навмисне делегування відповідальності за визначення типу компілятору, який завжди зробить це правильно.Тут криється важливий нюанс, який новачки часто пропускають. Коли ви пишете for (auto element : array), на кожній ітерації компілятор копіює черговий елемент масиву у змінну element. Для int чи double це незначна операція. Але уявіть масив великих структур або об'єктів — кожне копіювання матиме помітну вартість.
Щоб продемонструвати різницю, поглянемо на схему:
Щоб уникнути копіювання, достатньо додати & — і element стане посиланням (псевдонімом) на реальний елемент масиву, а не його копією:
#include <iostream>
int main()
{
int numbers[] = { 1, 2, 3, 4, 5 };
// Варіант 1: копія — зміни НЕ впливають на масив
for (auto num : numbers)
{
num = num * 2; // змінюємо лише локальну копію
}
std::cout << numbers[0] << '\n'; // Виводить: 1 (масив не змінився!)
// Варіант 2: посилання — зміни впливають на масив
for (auto& num : numbers)
{
num = num * 2; // змінюємо сам елемент масиву
}
std::cout << numbers[0] << '\n'; // Виводить: 2 (масив змінився!)
return 0;
}
Розбір:
for (auto num : numbers) — num є копією. Будь-які зміни num залишаються локальними та не впливають на вихідний масив.for (auto& num : numbers) — num є посиланням. Оператор num = num * 2 фактично змінює numbers[0], numbers[1] тощо.const auto& — золотий стандарт для читанняЯкщо ваша мета — лише прочитати елементи без їх зміни, найкращою практикою є const auto&:
const — гарантує, що ми випадково не змінимо елемент.& — запобігає зайвому копіюванню.auto — компілятор сам виводить тип.#include <iostream>
int main()
{
int scores[] = { 45, 87, 55, 68, 80, 90, 58 };
int total = 0;
for (const auto& score : scores) // читаємо без копіювання
{
total += score;
}
std::cout << "Сума: " << total << '\n';
return 0;
}
const auto& є правилом, якого варто дотримуватися за замовчуванням тоді, коли вам не потрібно змінювати елементи. Для невеликих скалярних типів (int, double, char) різниця між auto та const auto& в сучасних компіляторах мінімальна — вони самі оптимізують код. Але для великих об'єктів const auto& є справжньою необхідністю.
for (auto elem : arr)
elem не впливають на масив. Повільніше для великих об'єктів.for (auto& elem : arr)
elem змінюють масив. Без копіювання.for (const auto& elem : arr)
std::vector та іншими контейнерамиfor-each дозволяє ітерувати не лише по статичних масивах, а й по будь-якому контейнері стандартної бібліотеки — std::vector, std::string, std::array тощо. Синтаксис залишається ідентичним:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17 };
std::cout << "Прості числа: ";
for (const auto& prime : primes)
{
std::cout << prime << ' ';
}
std::cout << '\n';
return 0;
}
Зверніть увагу: для std::vector синтаксис абсолютно ідентичний синтаксису для звичайного масиву. Це одна з найважливіших переваг for-each — він абстрагує конкретний тип контейнера. Якщо ви вирішите замінити std::vector на std::array або будь-який інший стандартний контейнер — тіло циклу не зміниться.
for-each не працює з вказівникамиОсь де знають про себе знання з попередніх статей. Уявімо, що ми передаємо масив у функцію і хочемо підсумувати його елементи за допомогою for-each:
#include <iostream>
int sumArray(int array[]) // array тут — це int*, не масив!
{
int total = 0;
for (const auto& num : array) // ❌ Помилка компіляції
{
total += num;
}
return total;
}
int main()
{
int numbers[] = { 10, 20, 30, 40, 50 };
std::cout << sumArray(numbers) << '\n';
return 0;
}
Компілятор відкине цей код з помилкою на кшталт:
error: 'begin' was not declared in this scope
Чому? Щоб цикл for-each міг ітерувати, він повинен знати де починається і де закінчується діапазон. Для статичного масиву компілятор знає його розмір з оголошення. Але коли масив передається у функцію, відбувається Array Decay — масив «розпадається» у вказівник int*, і інформація про розмір безповоротно губиться. Вказівник знає лише адресу першого елемента, але не знає кількості елементів — тому for-each не може визначити, де зупинитися.
З цієї ж причини for-each не працює з динамічними масивами (new int[n]): змінна int* не несе в собі інформацію про розмір.
std::vector#include <iostream>
#include <vector>
// Варіант 1: передаємо розмір явно, використовуємо звичайний for
int sumWithSize(int array[], int length)
{
int total = 0;
for (int i = 0; i < length; ++i)
{
total += array[i];
}
return total;
}
// Варіант 2: std::vector — for-each працює, розмір відомий завжди
int sumVector(const std::vector<int>& vec)
{
int total = 0;
for (const auto& num : vec) // ✅ Працює!
{
total += num;
}
return total;
}
int main()
{
int numbers[] = { 10, 20, 30, 40, 50 };
std::vector<int> numVec = { 10, 20, 30, 40, 50 };
std::cout << "Сума (масив): " << sumWithSize(numbers, 5) << '\n';
std::cout << "Сума (vector): " << sumVector(numVec) << '\n';
return 0;
}
Одне з найчастіших запитань: «А як дізнатися індекс поточного елемента в for-each?». Коротка відповідь — ніяк безпосередньо. Це архітектурне рішення: for-each абстрагується від поняття індексу, адже багато контейнерів (наприклад, зв'язані списки) взагалі не підтримують довільного доступу за індексом.
Якщо індекс вам все ж потрібен, є два поширені підходи:
#include <iostream>
int main()
{
int values[] = { 10, 20, 30, 40, 50 };
// Підхід 1: окремий лічильник
int index = 0;
for (const auto& val : values)
{
std::cout << "values[" << index << "] = " << val << '\n';
++index;
}
std::cout << '\n';
// Підхід 2: якщо потрібен індекс — краще використати звичайний for
int length = 5;
for (int i = 0; i < length; ++i)
{
std::cout << "values[" << i << "] = " << values[i] << '\n';
}
return 0;
}
for-each є неоптимальним вибором для цієї задачі. Використовуйте звичайний for з явним індексом. Кожен інструмент має своє місце.Настав час систематизувати: коли for-each є правильним вибором, а коли — ні.
std::vector, std::string або інший стандартний контейнер.// ✅ Ідеальний випадок для for-each
int values[] = { 3, 1, 4, 1, 5, 9 };
int total = 0;
for (const auto& val : values)
total += val;
int* параметр функції (Array Decay).new int[n]).// ❌ for-each не підходить — потрібен індекс
int values[] = { 3, 1, 4, 1, 5, 9 };
for (int i = values[i - 1] != values[i]; i < 6; ++i)
// ...
// Краще: звичайний for
for (int i = 1; i < 6; ++i)
{
if (values[i] != values[i - 1])
std::cout << values[i] << '\n';
}
Рівень 1 — Базовий
Завдання 1. Оголосіть масив з 6 цілих чисел. Використайте for-each з auto, щоб вивести кожен елемент через пробіл. Потім повторіть з const auto& — поясніть різницю у написанні та результаті.
Завдання 2. Напишіть програму, що знаходить мінімальне значення в масиві double з 5 елементів за допомогою for-each. Використовуйте const auto&.
Завдання 3. Чому наступний код не компілюється? Запропонуйте два способи виправлення:
void printAll(int arr[])
{
for (const auto& num : arr) // ❌
std::cout << num << ' ';
}
Рівень 2 — Логіка
Завдання 4. Оголосіть масив int values[8] і заповніть його довільними числами. Використайте for-each з auto& (без const), щоб замінити всі від'ємні числа на нуль. Виведіть масив до і після змін.
Завдання 5. Реалізуйте функцію bool containsValue(const std::vector<int>& vec, int target), що повертає true, якщо вектор містить задане значення. Всередині використовуйте for-each з const auto& і оператор break для раннього виходу.
Завдання 6. Напишіть програму, яка за допомогою for-each підраховує кількість парних і непарних чисел у масиві з 10 елементів і виводить обидва лічильники.
Рівень 3 — Архітектура
Завдання 7. Реалізуйте функцію void replaceAll(std::vector<int>& vec, int oldValue, int newValue), що замінює всі входження oldValue на newValue у векторі. Використайте for-each з auto&. У main продемонструйте роботу: замініть усі 0 на -1 у векторі {1, 0, 2, 0, 3, 0}.
Завдання 8. Напишіть програму-«довідник імен»: оголосіть масив рядків const char* з 6–8 імен. Зчитайте ім'я з клавіатури (std::string). За допомогою for-each з const auto& перевірте, чи є таке ім'я в масиві, і виведіть відповідне повідомлення ("знайдено" або "не знайдено").
Синтаксис
for (const auto& elem : container)
// тіло циклу
Читається: «для кожного elem з container». Без індексів, без меж, без off-by-one.
Три форми
auto elem — копія, зміни не впливають на контейнер.auto& elem — посилання, можна змінювати.const auto& elem — ✅ рекомендовано для читання.Головне обмеження
int* (Array Decay) і динамічними масивами new int[n]. Компілятор не знає довжини — і не може визначити кінець діапазону.Коли обирати
for-each. Потрібен індекс / зворотний порядок / пропуск → звичайний for.У наступній статті ми розглянемо вказівники на функції — механізм, що дозволяє зберігати адресу функції у змінній і передавати її як аргумент, що є фундаментом для callback-функцій та гнучкого дизайну програм.
Оператор доступу до членів через вказівник (->)
Вивчіть оператор -> у C++ — синтаксичний цукор для доступу до членів структури через вказівник. Зрозумійте, чому (*ptr).member потребує дужок, як -> спрощує код і де він незамінний.
Вказівники на функції
Опануйте вказівники на функції у C++ — механізм, що лежить в основі callback-патернів, функціонального програмування та гнучкого проектування. Синтаксис, псевдоніми типів, std::function та практичні кейси.