У попередній статті ми вивчили лямбда-вирази — анонімні функції, що визначаються прямо в місці використання. Але є важлива деталь, яку ми навмисно відклали: лямбди з порожнім [] бачать лише те, що їм передано через параметри. Вони не мають автоматичного доступу до змінних навколишньої функції.
Розглянемо конкретну проблему. Є масив слів і порогове значення довжини — нам потрібно знайти перше слово, довша за порогове:
#include <iostream>
int strLen(const char* str)
{
int len = 0;
while (str[len] != '\0')
++len;
return len;
}
bool findFirstLonger(const char** words, int size, const std::function<bool(const char*)>& pred)
{
for (int i = 0; i < size; ++i)
if (pred(words[i]))
return true;
return false;
}
int main()
{
const char* words[] = { "hi", "morning", "ok", "sunshine", "bye" };
int minLength = 5; // ← ця змінна потрібна всередині лямбди
// ❌ Спроба звернутися до minLength — помилка!
auto isLong = [](const char* word) // [] — порожній capture, minLength невидима
{
return strLen(word) > minLength; // помилка компіляції: minLength not captured
};
return 0;
}
Компілятор повідомить про помилку: minLength знаходиться у зовнішній функції main, а лямбда не має до неї доступу. Це принципова відмінність від вкладених блоків коду, де змінні зовнішнього блоку видимі автоматично.
Причина в архітектурі: лямбда компілюється як окремий клас-функтор. Вона є незалежним об'єктом, а не «частиною» main. Щоб лямбда «побачила» зовнішні змінні — їх потрібно явно захопити.
[](params) { body }, типів лямбд (auto, std::function) і callback-патерну є обов'язковим.[variable]Найпростіший вид захоплення — за значенням. Лямбда отримує копію зовнішньої змінної прямо в момент свого визначення. Надалі лямбда працює зі своєю власною копією — зміни не впливають на оригінал і навпаки.
#include <iostream>
int strLen(const char* str)
{
int len = 0;
while (str[len] != '\0')
++len;
return len;
}
int main()
{
const char* words[] = { "hi", "morning", "ok", "sunshine", "bye" };
const int SIZE = 5;
int minLength = 5;
// [minLength] — захоплюємо minLength за значенням
// лямбда отримує КОПІЮ значення 5 прямо зараз
auto isLong = [minLength](const char* word)
{
return strLen(word) > minLength; // використовуємо свою копію
};
for (int i = 0; i < SIZE; ++i)
{
if (isLong(words[i]))
std::cout << words[i] << ' ';
}
std::cout << '\n';
// Змінюємо оригінальну змінну — лямбда цього «не відчує»
minLength = 10;
std::cout << "Після зміни minLength = 10:\n";
for (int i = 0; i < SIZE; ++i)
{
if (isLong(words[i]))
std::cout << words[i] << ' ';
}
std::cout << '\n';
return 0;
}
Розбір. Обидва виводи однакові — незважаючи на те, що minLength у main змінилась до 10, лямбда isLong продовжує використовувати свою копію зі значенням 5. Вона «сфотографувала» значення змінної в момент визначення і більше не залежить від оригіналу.
Ключова ідея: при захопленні за значенням лямбда — це незалежний знімок стану на момент визначення.
const — чому не можна змінюватиЗа замовчуванням захоплені за значенням змінні є константними всередині лямбди. Спроба їх змінити призведе до помилки компіляції:
#include <iostream>
int main()
{
int ammo = 10;
auto shoot = [ammo]()
{
--ammo; // ❌ Помилка: 'ammo' is const — захоплена за значенням
std::cout << "Pew! " << ammo << " shot(s) left.\n";
};
shoot();
return 0;
}
Логіка: захоплена копія є константою, тому що лямбда є функціональним об'єктом і не повинна мати прихований побічний ефект зміни «своїх» змінних (які є копіями, а не оригіналами).
mutableЯкщо вам дійсно потрібно змінювати захоплену за значенням змінну — позначте лямбду ключовим словом mutable. Воно знімає const з усіх захоплених за значенням змінних:
#include <iostream>
int main()
{
int ammo = 10;
// mutable — дозволяємо зміну захоплених за значенням змінних
auto shoot = [ammo]() mutable
{
--ammo; // ✅ Тепер можна — але змінюємо КОПІЮ, не оригінал!
std::cout << "Pew! " << ammo << " shot(s) left.\n";
};
shoot(); // Pew! 9 shot(s) left.
shoot(); // Pew! 8 shot(s) left.
shoot(); // Pew! 7 shot(s) left.
// ammo в main() ніяк не змінився!
std::cout << "Original ammo: " << ammo << '\n'; // 10
return 0;
}
Розбір по рядках.
Рядок 8. [ammo]() mutable — mutable стоїть після списку параметрів і перед тілом лямбди. Це дозволяє зміну захоплених за значенням змінних. Без mutable компілятор видасть помилку.
Рядок 10. --ammo — тут ammo — це копія, що належить лямбді. Лямбда запам'ятовує її стан між викликами. Перший виклик: копія ammo = 10 → 9. Другий виклик: копія ammo = 9 → 8. Третій: 8 → 7.
Рядок 17. ammo в main() залишається 10. Ми весь час змінювали копію, що живе всередині лямбди. Оригінал недоторканний.
mutable — не "вирішення проблеми". Якщо вам потрібно реально змінити зовнішню змінну — використовуйте захоплення за посиланням. mutable лише дозволяє лямбді мати власний мутований стан — копію, що «живе» разом з лямбдою.[&variable]Щоб лямбда дійсно впливала на зовнішню змінну — захопіть її за посиланням, додавши & перед ім'ям:
#include <iostream>
int main()
{
int ammo = 10;
// [&ammo] — захоплюємо ammo за посиланням
auto shoot = [&ammo]()
{
--ammo; // ← змінюємо оригінальну змінну з main()
std::cout << "Pew! " << ammo << " shot(s) left.\n";
};
shoot(); // Pew! 9 shot(s) left.
shoot(); // Pew! 8 shot(s) left.
std::cout << "Remaining ammo: " << ammo << '\n'; // 8
return 0;
}
Рядок 8. [&ammo] — замість копії лямбда отримує посилання на оригінальну змінну ammo. Це означає, що --ammo всередині лямбди і --ammo безпосередньо в main() — геть однакові за ефектом.
Зверніть: при захопленні за посиланням не потрібен mutable — посилання за своєю природою не має обмеження const.
Захоплення за посиланням досить часто використовується для «зовнішнього спостереження» за тим, що відбувається всередині алгоритму:
#include <iostream>
#include <utility> // для std::swap
void bubbleSort(int* array, int size, const std::function<bool(int, int)>& compare)
{
for (int pass = 0; pass < size - 1; ++pass)
for (int i = 0; i < size - pass - 1; ++i)
if (compare(array[i], array[i + 1]))
std::swap(array[i], array[i + 1]);
}
int main()
{
int numbers[] = { 5, 2, 8, 1, 9, 3 };
const int SIZE = 6;
int comparisons = 0; // зовнішній лічильник
// Захоплюємо comparisons за посиланням — щоб рахувати з лямбди
bubbleSort(numbers, SIZE, [&comparisons](int a, int b)
{
++comparisons; // кожен виклик лямбди — це одне порівняння
return a > b; // сортуємо за зростанням
});
std::cout << "Sorted: ";
for (int i = 0; i < SIZE; ++i)
std::cout << numbers[i] << ' ';
std::cout << '\n';
std::cout << "Total comparisons: " << comparisons << '\n';
return 0;
}
Рядки 20–24. Лямбда захоплює comparisons за посиланням і збільшує його на 1 при кожному виклику. bubbleSort нічого не знає про лічильник — він просто викликає compare(a, b). А завдяки &comparisons усі ці виклики «проходять» через оригінальну змінну в main(). Після сортування comparisons містить реальну кількість порівнянь.
Можна захоплювати кілька змінних одночасно, розділяючи їх комами. Кожну змінну можна захоплювати незалежно — одну за значенням, іншу за посиланням:
#include <iostream>
int main()
{
int health = 100;
int damage = 15;
int armor = 30;
// health і armor — за значенням (копії)
// damage — за посиланням (оригінал)
auto takeDamage = [health, armor, &damage]() mutable
{
int reduced = damage - armor; // застосовуємо зменшення через armor
if (reduced < 0) reduced = 0;
health -= reduced; // health — копія, зміна не впливає на оригінал
damage += 5; // damage — посилання, оригінал збільшується!
std::cout << "HP: " << health
<< ", reduced by " << reduced
<< ", next damage: " << damage << '\n';
};
takeDamage();
takeDamage();
takeDamage();
std::cout << "\nОригінали після викликів:\n";
std::cout << "health = " << health << " (незмінний — захоплено за значенням)\n";
std::cout << "damage = " << damage << " (змінився — захоплено за посиланням)\n";
std::cout << "armor = " << armor << " (незмінний — захоплено за значенням)\n";
return 0;
}
Розбір. [health, armor, &damage] — health і armor — копії, damage — посилання. Виклики takeDamage зменшують health-копію (оригінал лишається 100), але damage зростає при кожному виклику, бо він захоплено за посиланням — оригінальний damage в main() збільшується.
[=] і [&]Якщо лямбда використовує багато зовнішніх змінних, перелічувати кожну вручну незручно. Для цього існує захоплення за замовчуванням:
| Синтаксис | Призначення |
|---|---|
[=] | Захопити всі використовувані зовнішні змінні за значенням |
[&] | Захопити всі використовувані зовнішні змінні за посиланням |
#include <iostream>
int main()
{
int base = 10;
int multiplier = 3;
int offset = 7;
// [=] — всі три змінні захоплюються за значенням автоматично
auto calculateA = [=]()
{
return base * multiplier + offset; // всі три видимі
};
// [&] — всі три захоплюються за посиланням автоматично
auto calculateB = [&]()
{
base += 1; // змінюємо оригінали!
multiplier += 1;
return base * multiplier + offset;
};
std::cout << "A: " << calculateA() << '\n'; // 10*3+7 = 37
std::cout << "B: " << calculateB() << '\n'; // 11*4+7 = 51 (base і multiplier змінились)
std::cout << "base=" << base << ", multiplier=" << multiplier << '\n'; // 11, 4
return 0;
}
Правило спрощене: захоплення за замовчуванням можна змішувати зі звичайними — але кожна змінна може бути захоплена лише один раз, а захоплення за замовчуванням завжди перше:
int health = 100;
int armor = 50;
int damage = 30;
// [=, &damage] — all by value, except damage — by reference
auto deal = [=, &damage]() mutable { /* ... */ };
// [&, armor] — all by reference, except armor — by value
auto deal2 = [&, armor]() { /* ... */ };
// ❌ Заборонено — дублювання: armor вже захоплено через [=]
// auto bad1 = [=, armor]() {};
// ❌ Заборонено — захоплення за замовчуванням не першим
// auto bad2 = [armor, =]() {};
// ❌ Заборонено — подвійне захоплення armor
// auto bad3 = [armor, &health, &armor]() {};
[=] та [&] і завжди перераховувати змінні явно. Явне захоплення [minLen, &count] одразу показує читачеві, які саме зовнішні залежності має лямбда. [=] приховує цю інформацію.У capture-списку можна не лише захоплювати існуючі змінні, але й оголошувати нові — прямо в дужках:
#include <iostream>
int main()
{
int width = 6;
int height = 7;
// Нова змінна area, видима лише всередині лямбди.
// Вона обчислюється ОДИН РАЗ — при визначенні лямбди.
auto printArea = [area = width * height]()
{
std::cout << "Area: " << area << '\n';
};
// Змінюємо width і height — area всередині лямбди не зміниться!
width = 100;
height = 200;
printArea(); // Area: 42 — бо 6*7=42, обчислено при визначенні
return 0;
}
Рядок 9. [area = width * height] — це inicialized capture (C++14). Всередині [] ми оголошуємо нова змінна area і ініціалізуємо її виразом width * height. Вираз обчислюється одноразово — в момент визначення лямбди. Надалі width і height можуть змінитися як завгодно — area вже збережена.
Ще один варіант — захопити змінну з певною модифікацією:
#include <iostream>
int main()
{
int value = 5;
// Захоплюємо value, але подвоєне — нова змінна doubleValue
auto print = [doubleValue = value * 2]()
{
std::cout << "Double: " << doubleValue << '\n';
};
print(); // Double: 10
return 0;
}
Initialized capture — потужна можливість: вона дозволяє не лише повторно використовувати зовнішні значення, але і підготувати «трохи перетворені» версії прямо в специфікаторі захоплення — без зайвих проміжних змінних.
Захоплення за посиланням несе ризик «висячого посилання» (dangling reference) — якщо оригінальна змінна знищується раніше, ніж лямбда перестає існувати.
Найчастіша причина — функція повертає лямбду, яка захопила по посиланню локальну змінну функції:
#include <iostream>
#include <functional>
// Функція повертає лямбду — але & захоплює локальну змінну!
std::function<void()> makePrinter()
{
int localValue = 42; // локальна змінна — існує лише в makePrinter()
// [&] — захоплюємо localValue за посиланням
return [&]()
{
// ❌ Невизначена поведінка!
// localValue вже знищена після повернення з makePrinter()
std::cout << localValue << '\n';
};
}
int main()
{
auto printer = makePrinter(); // makePrinter() завершилась, localValue знищена
printer(); // ❌ UB: звертаємося до неіснуючої змінної
return 0;
}
42, 0, щось випадкове або аварійно завершитися — залежно від компілятора, оптимізацій і вмісту стеку. Жодна з цих відповідей не є «правильною» — UB є UB.Виправлення: захопіть за значенням:
std::function<void()> makePrinter()
{
int localValue = 42;
// [localValue] — копія, живе разом з лямбдою, не залежить від стеку makePrinter()
return [localValue]()
{
std::cout << localValue << '\n'; // ✅ безпечно
};
}
Правило: якщо лямбда «виходить» назовні (повертається з функції або зберігається довше за навколишній блок) — захоплюйте за значенням.
mutable-станомЛямбда — це об'єкт. При присвоєнні вона копіюється. Якщо лямбда має mutable-стан (захоплена за значенням змінна, яку вона змінює), то кожна копія отримує свою незалежну копію цього стану:
#include <iostream>
int main()
{
int counter = 0;
auto increment = [counter]() mutable
{
++counter;
std::cout << counter << '\n';
};
increment(); // 1 (counter лямбди: 0 → 1)
auto copy = increment; // копіюємо лямбду — копіюється і внутрішній стан!
// copy.counter = 1 (поточне значення)
increment(); // 2 (оригінал: 1 → 2)
copy(); // 2 (копія: 1 → 2, незалежно від оригіналу!)
return 0;
}
Замість очікуваного 1 2 3 ми бачимо 1 2 2. Чому? auto copy = increment копіює весь стан лямбди — включно зі внутрішньою копією counter (значення 1 на момент копіювання). Після цього increment і copy — два незалежні об'єкти зі своїми counter.
Аналогічна пастка з std::function:
#include <iostream>
#include <functional>
void callThreeTimes(const std::function<void()>& fn)
{
fn(); // std::function зберігає КОПІЮ лямбди всередині себе
fn(); // кожен виклик — одна й та сама копія
fn();
}
int main()
{
int counter = 0;
auto increment = [counter]() mutable
{
++counter;
std::cout << counter << '\n';
};
callThreeTimes(increment);
// Очікуємо: 1 2 3
// Реальність: 1 1 1 — std::function кожного разу виконує СВОЮ копію
return 0;
}
std::function при прийнятті лямбди копіює її всередину себе. Кожен виклик fn() виконує ту саму внутрішню копію — з тим самим початковим станом. Після кожного виклику стан оновлюється лише у внутрішній копії, яка при наступному виклику відновлюється знову.
Виправлення: якщо потрібно зберігати стан між викликами — захоплюйте за посиланням:
int counter = 0;
auto increment = [&counter]() // захоплення за посиланням — не копіюється
{
++counter;
std::cout << counter << '\n';
};
callThreeTimes(increment); // 1 2 3 — тепер правильно
Значення: `[x]`
x на момент визначення лямбди. Не відчуває змін оригіналу. Не може змінювати копію без mutable. Безпечно для «довгоживучих» лямбд.Посилання: `[&x]`
x. Зміни в лямбді → зміни оригіналу. Не потрібен mutable. Небезпечно, якщо оригінал знищується раніше лямбди.Всі за значенням: `[=]`
Всі за посиланням: `[&]`
Нова змінна: `[x = expr]`
x з результатом expr. Обчислюється один раз — при визначенні лямбди.`mutable`
const з захоплених за значенням змінних. Лямбда може змінювати свої копії. Але зміни не впливають на зовнішні оригінали.Рівень 1 — Базовий
Завдання 1. Що виведе код? Поясніть чому x у main не змінюється, а count змінюється:
int x = 10;
int count = 0;
auto fn = [x, &count]() mutable
{
++x;
++count;
std::cout << "x=" << x << " count=" << count << '\n';
};
fn();
fn();
std::cout << "main: x=" << x << " count=" << count << '\n';
Завдання 2. Визначте лямбду inRange, що приймає int і повертає true, якщо воно від min до max включно. min і max — зовнішні змінні, захоплені за значенням. Протестуйте з min=5, max=15 і числами {3, 7, 15, 20, 10}.
Завдання 3. Знайдіть і виправте помилку. Поясніть, чому вона виникає:
std::function<void()> makeCounter()
{
int n = 0;
return [&n]() { std::cout << ++n << '\n'; };
}
auto c = makeCounter();
c(); c(); c();
Рівень 2 — Логіка
Завдання 4. Реалізуйте функцію int sumIf(int* array, int size, std::function<bool(int)> pred), що підсумовує елементи, для яких pred повертає true. Протестуйте:
{1,2,3,4,5,6} → 12threshold, де threshold отримується із зовнішньої змінної через capture.Завдання 5. Напишіть функцію void forEachIndexed(int* array, int size, std::function<void(int, int)> fn), що викликає fn(index, value) для кожного елемента. Викличте її з лямбдою, що виводить лише індекси непарних елементів.
Завдання 6. Реалізуйте лямбду-акумулятор: кожен виклик додає передане число до внутрішньої суми і повертає поточну суму. Зовнішня змінна total повинна відображати актуальне значення після кожного виклику.
Рівень 3 — Архітектура
Завдання 7. Реалізуйте систему підрахунку статистики через лямбди із захопленням:
trackMin, trackMax, trackSum — кожна захоплює за посиланням відповідну змінну minVal, maxVal, total.void forEach(int* array, int size, std::function<void(int)> fn).forEach з масивом {7, 2, 14, 1, 9, 5, 11} і виведіть мін, макс і суму.| Синтаксис | Тип | Зміна зовнішньої? | Потребує mutable? |
|---|---|---|---|
[x] | Копія | ❌ Ні | ✅ Для зміни копії |
[&x] | Посилання | ✅ Так | ❌ Не потрібен |
[=] | Всі за копією | ❌ Ні | ✅ Для зміни копій |
[&] | Всі за посиланням | ✅ Так | ❌ Не потрібен |
[x = expr] | Нова змінна | — | ✅ Для зміни |
[=, &x] | Mix: все за копією, x за посиланням | Тільки x | Залежить |
[&, x] | Mix: все за посиланням, x за копією | Все крім x | ✅ Для x |
[&] у лямбді, що переживає свою функцію → висяче посилання; (2) mutable + копіювання лямбди → незалежні стани у кожної копії.У наступній статті ми переходимо до теми еліпсису (...) — синтаксису функцій зі змінною кількістю аргументів.
Лямбда-вирази
Вивчіть лямбда-вирази у C++11 — анонімні функції, що визначаються прямо в місці використання. Синтаксис, типи, auto-параметри, trailing-тип повернення, std::function та функтори STL.
Еліпсис
Вивчіть еліпсис (три крапки ...) у C++ — механізм функцій зі змінною кількістю аргументів. Синтаксис va_list/va_start/va_arg/va_end, три способи відстеження кількості аргументів, небезпеки та сучасні альтернативи.