1. Основи Watches: навіщо вони потрібні, що це і як не потонути в значеннях
Коли програма поводиться неправильно, перше бажання новачка — розставити десяток std::cout << "я тут був" << '\n'; і сподіватися, що баг злякається й утече. Іноді це спрацьовує, але частіше в результаті маємо «спам у консолі» й жодного розуміння, де саме і чому значення стало неправильним. Watches допомагає розв’язати саме цю проблему: важливі значення залишаються «перед очима» на кожній зупинці — без переписування коду й без захаращення виводу.
Важливо зрозуміти просту думку: відладчик цінний не тим, що показує «усі змінні підряд», а тим, що допомагає перевіряти гіпотези. У вас у голові має з’являтися фраза на кшталт: «Здається, індекс виходить за межі», або «Сума чомусь обнуляється», або «У цю функцію передали порожній рядок». Watches — це спосіб швидко зробити таку гіпотезу перевірюваною.
Для прикладів ми й далі розглядатимемо умовний навчальний консольний застосунок MiniBudget — найпростіший інструмент для обліку витрат: зберігаємо витрати в std::vector, додаємо записи, рахуємо суму, шукаємо за категорією. Жодних класів, ООП чи магії — тільки те, що ви вже знаєте: struct, функції, vector, string, цикли.
Що таке watch
Watch (спостереження) — це вираз, який відладчик намагатиметься обчислювати під час кожної зупинки й показувати вам його значення. Ключове слово тут — «вираз», а не просто «змінна». Watch може бути sum, а може бути i < v.size(), а може бути v[i] (але з нюансами), а може бути items.size().
Це відрізняється від вікна Locals: Locals зазвичай показує лише те, що є в поточному scope: параметри функції та локальні змінні. Watch ви обираєте самі, і він «живе» довше: ви можете зупинитися в іншому місці, а watch залишиться (хоча може стати not available, якщо вираз зараз поза областю видимості).
Корисна аналогія: Locals — це «що лежить на столі в поточного співробітника», а Watch — це «камери спостереження», які ви поставили там, де вам справді цікаво дивитися. І так само, як і з камерами, тут легко переборщити й потонути в потоці даних.
Головне правило: одна гіпотеза → кілька спостережень → висновок
Коли налагодження перетворюється на хаос, майже завжди причина в тому, що людина додала 25 watches «про всяк випадок». Відладчик починає показувати тонни чисел, і мозок вимикається. Тому тримаймо в голові просту дисципліну: одна гіпотеза → кілька спостережень → висновок.
Зазвичай достатньо 3–8 виразів. Наприклад, якщо ви налагоджуєте цикл за vector, то типовий мінімальний набір має такий вигляд:
| Що підозрюємо | Що поставити в watch | Навіщо |
|---|---|---|
| Вихід за межі | |
Швидко побачити момент, коли умова «зламалася» |
| Неправильна сума | |
Зрозуміти, на якому кроці сума стала неправильною |
| Хибна гілка if | cond, частини умови | Побачити, яка частина виразу дає false |
Зараз це здається «очевидним», але на практиці люди часто роблять навпаки: ставлять watch на v[i], але не додають i і v.size(). У підсумку — раптове cannot evaluate або падіння, і знову виникає відчуття, що «магія не працює».
2. Мініприклади: цикли, std::vector і std::string
Налагодження через Watches: вихід за межі та off-by-one у сумі
Коли ви рахуєте суму в циклі, класична помилка — межа i < n замість i <= n (або навпаки). На око код виглядає нормально, а результат — «трошки не той». Це ідеальний випадок для Watches: ми хочемо побачити, докуди реально дійшов i і як змінювалася сума.
Ось приклад функції в нашому MiniBudget, яка підсумовує перші n витрат (умовно — «топ-N»):
#include <cstddef>
#include <vector>
int sum_first_n(const std::vector<int>& amounts, std::size_t n) {
int sum = 0;
for (std::size_t i = 0; i < n; ++i) { // помилка: якщо n > amounts.size()
sum += amounts[i];
}
return sum;
}
Що додаємо в Watches, якщо бачимо дивний результат або падіння:
i, n, amounts.size(), i < amounts.size(), sum
І тут з’являється дуже важлива звичка: дивіться не лише на значення, а й на логічну коректність умов. Watch i < amounts.size() — це маленький «датчик інваріанта». Поки він true, ми відносно спокійні. Щойно він став false, ви знайшли точку, у якій програма починає працювати небезпечно.
Зауважте: ми не зобов’язані одразу лагодити код. Спершу фіксуємо факт: n виявився більшим за розмір контейнера, цикл дійшов до «зайвого» індексу, і далі все закономірно.
Watches для std::vector: як безпечно дивитися елементи
Із контейнерами є приємний бонус: багато IDE вміють гарно розкривати std::vector як список елементів. Але навіть якщо IDE робить це не надто добре, потрібні частини можна переглядати вручну. Головне — не створити собі ще одну помилку просто під час налагодження.
Погана звичка: ставити watch на v[i], коли i інколи буває «поганим». Тоді відладчик намагається обчислити вираз, а ви отримуєте або помилку обчислення, або потрапляєте в зону UB (залежно від того, як і де відладчик це робить).
Добра звичка: спочатку ставити «страхувальні» watches, а вже потім — елементи.
Наприклад, у нас є список витрат:
#include <string>
struct Expense {
std::string category;
int amount;
};
І пошук за категорією:
#include <cstddef>
#include <string>
#include <vector>
int find_first_by_category(const std::vector<Expense>& items, const std::string& cat) {
for (std::size_t i = 0; i < items.size(); ++i) {
if (items[i].category == cat) {
return static_cast<int>(i);
}
}
return -1;
}
Якщо пошук «інколи нічого не знаходить, хоча має», то watches, які справді допомагають, такі:
i, items.size(), cat
Також корисно додавати: items[i].category (але лише коли впевнені, що i < items.size()), і ще дуже корисно — items[i].category == cat як окремий вираз.
Тут ви швидко побачите типову ситуацію: наприклад, у даних категорія "Food " (із пробілом), а ви шукаєте "Food". І це якраз той момент, коли людина зазвичай годину сперечається з монітором, а watch показує правду за 5 секунд.
Watches для std::string: довжина, індекси й «де зламалося порівняння»
Рядки дуже часто збивають новачків з пантелику, бо візуально "abc" і "abc " — майже те саме (особливо якщо пробіл стоїть наприкінці). Watches дають змогу побачити це просто в момент порівняння.
Припустімо, у нас є функція «нормалізації» категорії: хочемо прибрати початкові й кінцеві пробіли (поки що примітивно, без складних алгоритмів):
#include <string>
std::string trim_one_space_right(std::string s) {
if (!s.empty() && s.back() == ' ') {
s.pop_back();
}
return s;
}
Якщо після «нормалізації» щось однаково не збігається, watches можуть бути такими:
s, s.size(), !s.empty()
А також s.back() (обережно: тільки якщо !s.empty()).
Дуже часта картина під час налагодження рядків: watch показує s.size() == 0, а ви все одно намагаєтеся дивитися s[0] або s.back(). Це саме той момент, коли відладчик ніби шепоче: «Не чіпай рядок, він порожній». І до нього варто прислухатися.
3. Нюанси: scope, stack frames і логічні watches
Watch і область видимості
Коли ви додаєте watch, інколи він стає сірим, червоним або not available. Новачок зазвичай думає: «Відладчик зламався». Насправді найчастіше причина прозаїчна: змінна вийшла з scope.
Приклад: змінна temp живе лише всередині if.
#include <iostream>
int main() {
int x = 10;
if (x > 0) {
int temp = x * 2;
std::cout << temp << '\n'; // 20
}
return 0;
}
Якщо ви поставили watch на temp, він буде доступний лише доти, доки виконання перебуває всередині блоку { ... }. Щойно ви вийшли з if, змінної більше немає, і watch чесно це показує.
Це не баг, а корисне нагадування: локальні змінні справді «живуть» рівно у своєму блоці. До речі, саме це знання потім дуже допомагає зрозуміти, чому деякі помилки то є, то нема.
Практичний висновок: якщо вам потрібно спостерігати за величиною довше, інколи варто тимчасово, для налагодження, винести змінну на рівень вище. Але робити це треба обережно: не перетворювати код на набір «змінних на початку функції про всяк випадок».
Watches і stack frames
Після лекції про call stack стає зрозумілішим один важливий нюанс: watch-вираз обчислюється в контексті поточного frame. Якщо ви перемкнулися на інший frame у стеку, то «ті самі» імена можуть означати вже щось інше.
Уявімо, що add_expense() читає рядок, парсить число через parse_amount(), і там щось пішло не так:
#include <string>
int parse_amount(const std::string& s) {
int x = 0;
// припустімо, тут є помилка парсингу
return x;
}
void add_expense(const std::string& line) {
int amount = parse_amount(line);
(void)amount;
}
Якщо ви зупинилися всередині parse_amount, watch s — це параметр parse_amount. Якщо ви перемкнетеся на frame вище (в add_expense), watch line — це інший параметр, хоча за змістом він «про те саме». А якщо у двох функціях у вас є змінна x, то watch x легко перетвориться на «лотерею», якщо ви не стежите за frame.
Тому ось невелике правило дисципліни: перш ніж довіряти watches, переконайтеся, що дивитеся на правильний stack frame.
Логічні watches: стежимо за інваріантами
Часто проблема не в конкретному значенні, а в порушенні правила. Наприклад: «індекс завжди менший за size()», «сума не повинна бути від’ємною», «категорія не повинна бути порожньою».
Для цього зручно ставити watch не на «сирі дані», а на булеві вирази. Це перетворює налагодження на пошук моменту, коли правило стало false.
Мініприклад з інваріантом «amount має бути > 0»:
#include <vector>
int total_positive(const std::vector<int>& a) {
int sum = 0;
for (int x : a) {
sum += x; // підозріло, якщо x буває від’ємним
}
return sum;
}
Корисні watches тут: x, sum, і вираз x > 0. Якщо x > 0 раптово стало false, то питання вже не «чому сума не та», а «чому в контейнер потрапили від’ємні витрати».
Так ви поступово починаєте налагоджувати не «значення», а контракти. І це дуже доросла звичка, навіть якщо ви поки що початківець.
4. Сценарій: налагоджуємо MiniBudget через watches
Уявімо ситуацію: користувач вводить витрати, а підсумкова сума чомусь менша за очікувану. Ми не будемо зараз сперечатися з користувачем (хоча це теж навичка), а зробимо те, що робить розробник: ставимо гіпотезу й перевіряємо її.
Нехай у нас є функція підсумовування:
#include <vector>
int total(const std::vector<int>& a) {
int sum = 0;
for (std::size_t i = 0; i < a.size(); ++i) {
if (a[i] > 0) {
sum += a[i];
}
}
return sum;
}
Виявилося, що інколи витрати вводяться як від’ємні (наприклад, повернення), а користувач очікує, що «за модулем» це теж буде враховано. Логіка програми може не відповідати вимогам, але для нас зараз важливіша сама механіка налагодження.
Ставимо breakpoint на рядок sum += a[i];, і в watches додаємо:
i, a.size(), a[i], a[i] > 0, sum
Далі натискаємо Continue до першої зупинки й дивимося: a[i] > 0 часто false, отже ця гілка пропускає частину значень. Ось і причина «менше за очікувану»: програма підсумовує лише додатні.
Ключовий момент: ми дійшли висновку на основі спостережуваних фактів, а не зі здогаду «мабуть, цикл працює неправильно». І це саме те, заради чого існує вікно watch.
Невелика схема процесу (по-людськи, без фанатизму):
flowchart TD
A[Гіпотеза: сума губиться в циклі] --> B[Ставимо breakpoint у підозрілому місці]
B --> C[Додаємо 3–8 watches: i, size, елемент, умова, sum]
C --> D[Continue/Step і дивимося, де саме відбувається зміна]
D --> E[Фіксуємо факт: яка гілка або яке значення ламає очікування]
5. Типові помилки під час роботи з Watches
Помилка № 1: перетворювати вікно watch на «звалище всього підряд».
Коли ви додаєте 20–30 виразів, то починаєте дивитися не на програму, а на шум. У підсумку мозок перестає помічати головне: де саме значення змінилося і яке правило порушилося. Краще дотримуватися дисципліни: один набір watches для однієї гіпотези, потім чистимо й ставимо новий.
Помилка № 2: watch на v[i] без контролю меж.
Це класика жанру: людина підозрює вихід за межі й насамперед додає v[i]. Але якщо i уже неправильний, то вираз сам по собі стає небезпечним і марним. Набагато розумніше спочатку спостерігати i, v.size() і i < v.size(), і лише коли інваріант дотримано — дивитися v[i].
Помилка № 3: забувати про область видимості.
Watch може «зникнути», бо змінної більше не існує: ви вийшли з блока {} або перемкнули stack frame. Це не проблема відладчика, а чесне відображення моделі мови: локальна змінна живе рівно там, де ви її оголосили. Якщо watch став недоступним — спершу перевірте, де ви перебуваєте і який frame вибрано.
Помилка № 4: робити висновки, не прив’язуючи спостереження до кроку й рядка.
Іноді студент бачить sum = 42 і одразу вирішує, що «ось тут помилка». Але без прив’язки до конкретного рядка й моменту зміни це просто число. Хороша практика — помічати, на якому кроці й після якого рядка значення стало іншим. Watches добрі тим, що дають змогу бачити динаміку на кожній зупинці, але висновок однаково має бути прив’язаний до конкретного місця виконання.
Помилка № 5: намагатися налагоджувати без гіпотези.
Це схоже на спробу лікувати застуду, дивлячись на термометр кожні 5 секунд, але не розуміючи, що саме ви перевіряєте. Якщо ви не можете сформулювати перевірюване твердження на кшталт «індекс не виходить за межі» або «умова має стати true на цій ітерації», то watches не врятують — вони покажуть числа, але не дадуть відповіді. Спершу формулюємо гіпотезу, потім обираємо спостереження.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ