Українська
Розбори та типові помилки
Тести алгебраїчних властивостей
Окрім одного прикладу 1/2 + 1/3, перевірте скорочення, знак знаменника, нуль, відмову нульового знаменника й межі діапазону. Рівність має бути рефлексивною, симетричною та транзитивною для допустимих значень. Результати <, == і > не повинні суперечити одне одному для типу із сильним порядком.
Для чисел із плаваючою крапкою математичні тотожності можуть не виконуватися буквально через округлення. Не обіцяйте точну асоціативність додавання double. Обирайте прості точно подані дані для структурних тестів або явно визначайте допустиму похибку.
Для введення перевірте повну пару, відсутній другий компонент, нечисловий текст і нескінченність; після відмови порівняйте ціль зі старим значенням. Для індексування перевірте перший, останній і перший недопустимий індекс, а також константний об’єкт.
Порівняйте префіксну й постфіксну форми окремо: обидві змінюють стан, але повертають різні значення. Для симетричного множення перевірте обидва порядки операндів. Так тести перевіряють контракт інтерфейсу, а не лише повторюють формулу з тіла функції.
Проєктування операцій для температури та різниці температур
Не кожне знайоме число має звичайну арифметику. Температура точки та зміна температури – різні поняття. Додавання двох абсолютних температур зазвичай не має того змісту, який очікує користувач; натомість додавання різниці до температури визначає нову температуру. Віднімання двох температур дає різницю. Це можна виразити двома типами.
Таке проєктування змушує обрати сигнатури до реалізації. Операція Temperature - Temperature повертає TemperatureDelta, а Temperature + TemperatureDelta повертає Temperature. Для переведення Celsius у Fahrenheit абсолютна температура містить зсув, тоді як різниця використовує лише масштаб. Один універсальний double не допомагає компілятору помітити змішування цих випадків.
Подібна ситуація виникає для календарної дати й тривалості, координати точки й вектора зміщення, грошової суми й відсотка. Правильно обрані типи зменшують кількість допустимих, але беззмістовних виразів. Перевантаження тоді служить обмеженню помилок, а не лише скороченню коду.
Коли додаєте нову операцію, сформулюйте одиниці обох аргументів і результату. Для distance / time результат має одиницю швидкості; нульовий час потребує відмови. Для speed * time результат знову є відстанню. Перевірки мають включати одиниці й межі, а не лише числову формулу, бо неправильна одиниця дає цілком правдоподібне число.
Узгодженість рівності, порядку та нормалізації
Дріб 1/2 і 2/4 представляють одне значення. Якщо клас залишає обидві форми без нормалізації, порівняння полів поверне нерівність. Можна реалізувати рівність через перехресні добутки, але тоді треба контролювати переповнення в кожному порівнянні. Нормалізація один раз під час створення спрощує наступні операції.
Канонічний нуль також важливий. Значення 0/3 та 0/7 мають стати 0/1. Знак зберігається в чисельнику, знаменник додатний. Тепер друк, рівність і тестування мають одне стабільне подання. Це не доводить, що будь-яка арифметика безпечна: проміжні добутки все ще потребують обмежень або ширшого обчислення.
Для кольору RGB рівність компонентів природна, але порядок «менше» неоднозначний. Лексикографічний порядок, яскравість і номер у палітрі – різні політики. Якщо задача не потребує універсального порядку кольорів, не варто додавати spaceship лише тому, що він коротко записується. Іменований компаратор для конкретного сортування може бути точнішим.
Для double NaN не дорівнює самому собі й породжує unordered. Це не помилка компілятора, а частина моделі плаваючої крапки. Клас, який хоче сильного порядку, має або виключити такі значення з інваріанта, або явно визначити іншу політику. Не можна просто оголосити strong_ordering і проігнорувати невпорядкований випадок.
Таблиця тестів інтерфейсу операцій
| Операція | Властивість | Помилковий випадок |
|---|---|---|
a + b | Не змінює a та b | Перевищення діапазону |
a += b | Повертає посилання на a | Стан після відмови |
a == b | Симетричність і транзитивність | Різні подання того самого значення |
a <=> b | Узгодженість із рівністю | NaN або неповний порядок |
x++ | Результат є старим значенням | Максимальне значення |
++x | Результат посилається на новий стан | Переповнення |
in >> x | Повний результат або незмінний x | Неповний текст |
out << x | Повертається той самий потік | Небажана зміна flags |
Таблиця дозволяє перевірити поведінку незалежно від конкретного алгоритму. Наприклад, якщо operator+ реалізовано через +=, тест незмінності обох аргументів все одно корисний: випадкове приймання лівого аргументу за неконстантним посиланням порушить контракт, хоча сума на екрані буде правильною.
Для введення варто перевірити повторне використання того самого об’єкта: спочатку прочитати коректне значення, потім некоректне. Інакше тест на новому нульовому об’єкті може не помітити часткового присвоєння першого компонента. Саме послідовність станів виявляє помилку, яку не видно в одному рядку очікуваного виведення.
Контракт інтервалу: операція не завжди повертає той самий тип
Перетин двох напіввідкритих інтервалів може бути порожнім. Якщо тип вимагає строго додатну довжину, результат не завжди можна представити цим типом. Потрібно або дозволити порожній інтервал, або повернути optional, або обрати інший явний контракт. Нульова довжина не повинна випадково маскувати помилку конструктора.
Об’єднання двох несуміжних інтервалів не є одним інтервалом. Повернення від мінімального початку до максимального кінця створює обмежувальний інтервал і включає проміжок, якого не було у вході. Тому операцію слід назвати відповідно або повернути набір частин. Знайомий символ не виправляє невідповідність математичному змісту.
Подібне питання виникає для прямокутників. Символ | у завданні явно означає обмежувальний прямокутник, а не точну геометричну область об’єднання. Така домовленість має бути видимою в умові, документації й тестах. Інакше користувач може отримати правильну реалізацію неправильного очікування.
Чому конверсії краще робити явними
Клас Money із неявним конструктором від цілого дозволив би додавати до суми будь-яке ціле число, але не пояснив би його одиницю: гривні чи копійки. Explicit-конструктор вимагає зробити вибір видимим у коді. Це невелика додаткова довжина заради запобігання дорогій предметній помилці.
Так само operator int для оцінки може відкрити беззмістовну арифметику, якщо перетворення неявне. Явний оператор дозволяє отримати число там, де це свідомо потрібно, але не підміняє всі перевантаження, що випадково приймають int. Контекстна конверсія bool для умов є окремою зручною можливістю explicit operator bool.
Коли кілька неявних перетворень утворюють ланцюжок кандидатів, повідомлення про неоднозначну операцію стає складнішим. Мінімальний набір конверсій полегшує і вибір перевантаження, і читання коду. Не додавайте перетворення лише для того, щоб один тест компілювався без явного зазначення одиниць.
Два рівні перевірки потокового оператора
Перший рівень перевіряє текст: operator<< для дробу має надрукувати нормалізовані чисельник і знаменник у визначеному форматі. Другий рівень перевіряє інтеграцію з потоком: повертається той самий ostream, ланцюжок працює, зайве перенесення рядка не з’являється, сторонні параметри форматування не псуються.
Для operator>> перший рівень – правильне значення після коректного тексту. Другий – стан failbit і незмінність цілі після неповного чи помилкового входу. Якщо читач приймає лише два числа через пробіл, тест не повинен мовчки очікувати підтримку дужок або символу i.
Текстовий формат може згодом стати форматом файла. Тоді стабільність і однозначність важливіші за декоративність. Виведення для людини й серіалізація можуть вимагати різних функцій. Не обіцяйте round-trip лише через однакові імена << та >>: перевірте, що надрукований текст справді читається назад без втрати потрібного значення.