Українська
Розбори та типові помилки
Покроковий розбір конвеєра
Нехай джерело містить 1,2,3,4,5,6,7,8, а конвеєр відбирає парні, обчислює квадрат і бере три результати. Перший запит результату змушує filter пропустити 1 і знайти 2; transform повертає 4. Наступний перехід пропускає 3 і використовує 4, тому результат – 16. Третій результат – 36 із джерельного 6. Обмеження take означає, що значення 8 вже не потрібне споживачеві для отримання трьох відповідей.
Проте не слід робити з цієї траси нормативний підрахунок усіх внутрішніх викликів. Ітератор може розіменовуватися кілька разів, а алгоритм може окремо запитувати межу або відстань. Наведена траса пояснює потік значень, а не гарантує один виклик кожної лямбди. Для дорогого або недетермінованого обчислення інколи краще матеріалізувати результат один раз і працювати з ним.
Таблиця 14.1. Порядок адаптерів є частиною задачі
| Конвеєр | Значення | Зміст |
|---|---|---|
| filter → take(3) | 2,4,6 | Перші три придатні |
| take(3) → filter | 2 | Придатні серед перших трьох |
| transform → take(3) | 1,4,9 | Квадрати перших трьох |
| reverse → take(3) | 8,7,6 | Останні три у зворотному порядку |
Якщо джерело є map, values не копіює всі значення в новий vector. Воно дає доступ до відповідних компонентів пар. Зміна значення через змінюване посилання відбивається у map. Для звіту-знімка, який не повинен змінюватися після редагування сховища, потрібне явне копіювання у власний контейнер.
Різниця між об’єктом view і його елементами
Копіювання невеликого view часто копіює лише опис обходу та посилання на джерело, а не всі елементи. Два view можуть бачити ту саму пам’ять. Якщо один цикл змінює дані, другий може отримати нові значення або порушений кеш початку. Правило «копія означає незалежні дані» для таких об’єктів не працює, тому перевірка часу життя має включати саме власника.
Водночас власний vector рядків, отриманий через to після перетворення на string, уже володіє символами. Це не те саме, що vector<string_view>: другий володіє лише маленькими описами позичених фрагментів. Вибір типу елемента після матеріалізації важливіший за сам факт наявності vector.
Контракти числових алгоритмів на конкретних значеннях
Для послідовності дійсних чисел {0.5, 0.5, 0.5} виклик accumulate з початковим int 0 може втрачати дробову частину на кожному кроці. Початковий double 0.0 виражає іншу політику накопичення. Та сама проблема виникає, якщо лямбда явно повертає int або проміжний добуток обчислюється в занадто вузькому цілому типі до додавання в ширший.
Для inner_product коефіцієнтів {2,3,4} і степенів {1,2,4} результат дорівнює 24. Якщо другий масив має лише два значення, класичний інтерфейс не перевірить його кінець. Не намагайтеся «виправити» це довільним скороченням першого масиву: математичний поліном тоді зміниться. Правильна політика – відхилити несумісні розміри або свідомо визначити нульове доповнення.
Для суми грошей у копійках точність цілої арифметики не означає відсутності переповнення. Перед обчисленням межі суми треба оцінити з кількості й максимального значення. Для середнього бала потрібен дійсний поділ, але сама сума може накопичуватися точно в достатньо широкому цілому типі. Технічний вибір типу походить із діапазону даних задачі.
Стабільність і детермінований звіт
Нехай початкові записи (A,90),(B,80),(C,90) уже впорядковані за часом реєстрації. Stable_sort за спаданням бала залишить A перед C. Звичайний sort має право поміняти їх місцями. Якщо потрібен порядок за ім’ям, стабільність сама по собі його не забезпечує: початковий порядок міг бути іншим. Вкажіть другий критерій або послідовність стабільних сортувань.
Сортування за кількома полями не слід записувати як «перший бал більший АБО ім’я менше» без перевірки нічиєї. Така умова може дати true в обох напрямках для двох записів і порушити строгий порядок. Спочатку порівнюють основний критерій; лише за його еквівалентності використовують другий. Перевірте компаратор на однаковому записі та на трьох записах для транзитивності.
Перевірка часу життя перед поверненням результату
Функція, що створила локальний vector, не повинна повертати view, який посилається на нього. Після завершення функції елементи знищені, а дешевий об’єкт-подання все ще може містити адреси. Замість цього поверніть власний vector, передайте джерело від викликача з явним контрактом життя або використайте коректний власний діапазон.
Подібна пастка виникає з предикатом, який захопив локальну межу за посиланням. Навіть якщо сам vector належить викликачеві й живе достатньо довго, filter зберігає нечинне посилання на межу. Копія невеликого числа в захопленні [limit] вирішує саме цю залежність; вона не виправляє життя самого джерела.
Створюючи API, запишіть відповідь на три питання: хто володіє елементами, хто володіє станом предиката і які операції можуть зробити позиції недійсними. Якщо відповідь довга та незрозуміла, власний результат часто кращий за мінімізацію копій. Оптимізація не повинна приховувати необхідні передумови від користувача функції.
Матриця тестів для алгоритмічного рішення
Для відбору перевірте: жоден елемент не проходить, усі проходять, проходить лише перший, лише останній та порожній набір. Для сортування додайте вже впорядкований набір, зворотний порядок, дублікати й однакові ключі з різними додатковими полями. Для пошуку перевірте значення перед мінімумом, між елементами й після максимуму.
Для chunk перевірте розмір 1, рівне ділення і коротку останню групу; нульову місткість відхиліть до адаптера. Для zip порівняйте рівні й нерівні довжини згідно з контрактом задачі. Для split окремо перевірте сусідні роздільники та роздільник наприкінці. Зафіксуйте очікувані результати до запуску, а не пояснюйте випадковий результат після виконання.
Нарешті, перевірка власного ітератора повинна включати не лише static_assert концепту. Перевірте розіменування, префіксний і постфіксний перехід, досягнення sentinel та відсутність виходу за межі. Концепт перевіряє доступність виразів, але не доводить, що оператор++ справді переходить до правильного наступного значення.
Вибір алгоритму за потрібним результатом
Перед написанням циклу сформулюйте результат одним реченням. Пошук першого підхожого елемента, підрахунок усіх підхожих і створення нового набору – різні операції. Однакова умова в лямбді не робить ці задачі тотожними. Таблиця допомагає відокремити споживання діапазону від зміни його структури.
Таблиця 14.2. Алгоритм, результат і передумова
| Потрібно | Засіб | Критична передумова |
|---|---|---|
| Перший відповідний | find_if | Перевірити межу перед читанням |
| Кількість відповідних | count_if | Предикат не змінює критерій |
| Чи всі відповідають | all_of | Порожній діапазон дає true |
| Нова копія відбору | copy_if | Достатній вихід або back_inserter |
| Лінивий відбір | views::filter | Власник і стан предиката живі |
| Повне впорядкування | sort | Довільний доступ і строгий порядок |
| Збереження нічиїх | stable_sort | Потрібний початковий порядок |
| Перші k найменших | partial_sort | Межа k у допустимих межах |
| Глобальні дублікати | sort + unique + erase | Допустима зміна порядку |
| Пошук позиції вставки | lower_bound | Узгоджений порядок |
| Послідовна сума | accumulate / fold_left | Правильний початковий тип |
| Пари елементів | views::zip | Перевірити потрібну рівність довжин |
Алгоритм не повинен підміняти доменну перевірку. Наприклад, all_of підтверджує, що всі наявні оцінки допустимі, але не підтверджує, що оцінки взагалі введені. Так само відсутність результату find може бути нормальною відповіддю або помилкою даних – це визначає умова задачі. Явно обрана реакція на порожній і відсутній результат робить інтерфейс передбачуваним.