JavaRush /Курси /C++ SELF /static_assert: перевірки під час компіляції

static_assert: перевірки під час компіляції

C++ SELF
Рівень 35 , Лекція 4
Відкрита

1. Навіщо потрібен static_assert

Якщо assert — це «охоронець на вході до небезпечної ділянки коду», то static_assert — «охоронець на вході до етапу компіляції». Порівняння, можливо, звучить трохи різко, але воно дуже практичне: деякі правила коректності можна перевірити ще під час компіляції. Якщо їх порушено, краще одразу отримати помилку збирання, ніж потім ловити дивні баги під час виконання, особливо в Release. У сучасному C++ static_assert — це вбудований механізм мови, фактично ключове слово, а не бібліотечний трюк. І компілятор ставиться до нього максимально серйозно.

Уявіть, що у вашому проєкті є константа, яка задає «налаштування»: розмір буфера, максимальну довжину рядка, ширину таблиці, кількість спроб або крок сітки. У певний момент ви змінюєте її навмання — і раптом половина програми починає поводитися дивно. За допомогою static_assert ви можете сказати: «ця константа зобовʼязана бути додатною», «зобовʼязана бути парною», «зобовʼязана бути не меншою за 10». І якщо хтось, зокрема й ви самі за тиждень, порушить це правило, компілятор зупинить збирання зі зрозумілим повідомленням.

Синтаксис і базовий приклад

Приємно те, що static_assert майже не потребує контексту: це один рядок, який читається як українське речення — «стверджую, що умова істинна». Найчастіше використовують форму з двома аргументами: умова та текст помилки. Текст помилки — не прикраса, а ваше повідомлення в майбутнє: «я з минулого пояснюю, чому тут не можна ставити 0». Формально у static_assert є і форма без повідомлення, але для новачків вона майже завжди менш зручна, бо текст істотно спрощує читання помилки компіляції.

Базовий шаблон:

static_assert(умова, "повідомлення про помилку");

Важливо: умова має обчислюватися на етапі компіляції. Тобто компілятор має вміти сказати «true/false», не запускаючи програму. Такі вирази називають constant expression (константний вираз). На цьому етапі достатньо памʼятати просте правило: літерали, constexpr-константи, sizeof(...), арифметика над ними — зазвичай підходять.

Мініприклад, найбільш «підручниковий»:

static_assert(2 + 2 == 4, "Математика зламалася. Паніка."); // усе гаразд

Якщо умова хибна, програма не компілюється. Вона не «падає» і не «виводить помилку» — вона просто не збирається.

2. static_assert, assert і if: коли що використовувати

У цей момент у новачків часто виникає спокуса: «О! Тепер я завжди писатиму static_assert замість assert». Це приблизно як намагатися забивати цвяхи мікроскопом: теоретично можна, але потім шкода мікроскоп. static_assert і assert перевіряють різні речі й спрацьовують у різний час: один — під час компіляції, інший — під час виконання. А звичайний if — це взагалі інша історія: не діагностика, а звичайна гілка логіки, яку користувач цілком може активувати «на законних підставах».

Зручно порівняти це в таблиці:

Інструмент Коли спрацьовує Що перевіряємо Що відбувається у разі порушення
static_assert(cond, msg)
під час компіляції правила, відомі заздалегідь (константи, розміри, властивості типів тощо) збирання зупиняється, виникає помилка компіляції
assert(cond)
під час запуску (зазвичай Debug) внутрішні інваріанти, які «у нормі» завжди істинні аварійне завершення, часто з координатами помилки
if (...) { ... } else { ... }
під час запуску очікувані ситуації (наприклад, хибне введення) програма продовжує роботу за відповідною гілкою

І ось ключова думка: static_assert потрібен тоді, коли помилка — це помилка програміста або конфігурації, а не «проблема користувача». Користувач не винен, що ви поставили kMaxNameLen = -5. Це ваша зона відповідальності, і компілятор може допомогти виявити проблему раніше.

3. Практика: TaskBook і перевірка конфігурації

Щоб static_assert не здавався академічною магією, давайте вбудуємо його в наш консольний застосунок. Нехай він називається TaskBook: це простий менеджер завдань у консолі. Ми вже вміємо зберігати завдання в std::vector, читати рядки, друкувати таблицю та обробляти команди. Тепер додамо «конфігурацію» — набір констант, які впливають на формат і обмеження. І додамо static_assert, щоб ці налаштування не перетворилися на міну сповільненої дії.

Уявімо файл config.hpp:

// config.hpp
#pragma once

constexpr int kMaxTitleLen = 40;
constexpr int kTableWidth  = 60;

static_assert(kMaxTitleLen > 0, "kMaxTitleLen має бути додатним");
static_assert(kTableWidth >= kMaxTitleLen + 10, "kTableWidth замалий");

Тут відбувається важлива річ: ми не просто «сподіваємося», що ширина таблиці адекватна. Ми забороняємо збирати програму в неправильній конфігурації.

Тепер у main.cpp ми використовуємо ці значення:

#include <iostream>
#include <string>
#include "config.hpp"

int main() {
    std::string title = "Buy milk";
    std::cout << "Максимальна довжина заголовка = " << kMaxTitleLen << '\n'; // Максимальна довжина заголовка = 40
    std::cout << "Ширина таблиці                = " << kTableWidth << '\n';  // Ширина таблиці = 60
}

Суть у тому, що якщо хтось «оптимізує» kTableWidth до 20 (бо «ну мені так красивіше»), компілятор зупинить збирання і прямо скаже: "kTableWidth замалий". І це станеться ще на етапі компіляції: до запуску, до тестів і до запитання «а чому в релізі поїхало верстання в консолі?».

4. Що вважається умовою, відомою під час компіляції

Коли кажуть «має обчислюватися на етапі компіляції», спершу може здатися, що це якась езотерика. Насправді все простіше: компілятор уміє обчислювати вирази, які не залежать від введення користувача, файлів, мережі й узагалі від самого виконання програми. Тобто все, що «статичне».

На вашому поточному рівні корисно запамʼятати кілька типових джерел таких значень: constexpr константи, літерали, sizeof, розмір std::array, межі типів (часто через <limits>, якщо вони constexpr), і просту арифметику з усім цим.

Подивімося на кілька коротких прикладів, які справді трапляються в прикладному коді.

Перевірка розмірів типів через sizeof

Іноді проєкт спирається на просте припущення: наприклад, що int принаймні 32-бітний. Зазвичай це так, але static_assert дає змогу сформулювати це як контракт.

#include <cstddef>

static_assert(sizeof(int) >= 4, "Ця програма очікує, що int буде принаймні 32-бітним");

Ми не заглиблюємося в платформні нетрі, але ідея проста: якщо припущення хибне, нехай збирання зупиниться.

Перевірка розміру фіксованого масиву й буфера

Припустімо, у TaskBook ми хочемо зробити невеликий буфер для команди (так, у нас уже є std::string, але приклад навчальний). Перевірмо, що його розмір розумний.

constexpr std::size_t kCmdBufferSize = 128;

static_assert(kCmdBufferSize >= 16, "Буфер команд замалий");
static_assert(kCmdBufferSize <= 1024, "Буфер команд підозріло великий");

Це типовий «конфігураційний» підхід: ми задаємо діапазон допустимих значень.

Перевірка формату: парність і межі

Наприклад, під час виведення таблиці ми хочемо, щоб ширина була парною — скажімо, для красивого центрування. Це не обовʼязково, але як приклад підходить чудово.

constexpr int kTableWidth = 60;
static_assert(kTableWidth % 2 == 0, "kTableWidth має бути парною для гарного макета");

5. Де static_assert не працює

Дуже важливо вчасно зрозуміти, де закінчуються можливості static_assert. Якщо ви спробуєте перевіряти ним те, що надходить від користувача, компілятор не «образиться» — він просто не зможе цього зробити. Бо значення зі std::cin зʼявляється лише під час запуску, а static_assert живе раніше — у момент компіляції.

Неправильний приклад (ми його ізолюємо, щоб не ламати збирання):

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

#if 0
    static_assert(x > 0, "x має бути додатним"); // не можна: x відомий лише під час виконання
#endif

    std::cout << x << '\n';
}

Якщо вам потрібно перевірити введення користувача, це звичайна логіка програми, і робиться вона через if (а інколи через assert, якщо це внутрішня умова; але введення майже ніколи не є інваріантом).

Правильний варіант для перевірки введення:

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

    if (x <= 0) {
        std::cout << "Будь ласка, введіть додатне число\n";
        return 0;
    }

    std::cout << "OK: " << x << '\n'; // OK: 5
}

І ось тут зʼявляється хороше «архітектурне» чуття: static_assert перевіряє те, що ми контролюємо як розробники, а if — те, що контролює зовнішній світ (користувач, файли, мережа, середовище).

6. static_assert і препроцесор #if

На перший погляд static_assert і #if інколи розвʼязують схожі завдання: «щось перевірити до виконання». Тому новачки часто починають їх плутати.

Важливо чітко розвести сенси: #if — це препроцесор, який працює з текстом і макросами, а static_assert — частина мови, яка перевіряє саме C++-вираз. Препроцесор не «розуміє» типи, області видимості та constexpr: він просто вирізає або склеює текст. static_assert натомість розуміє мову, і тому краще підходить для перевірок логіки та контрактів.

Наприклад, такий код виглядає як «ніби перевірка», але насправді це суто препроцесорна історія:

#define TABLE_WIDTH 60

#if TABLE_WIDTH < 20
#error TABLE_WIDTH замалий
#endif

Працює? Так. Але це макроси, а з ними ви втрачаєте типову безпеку, області видимості й узагалі все те, за що цінуємо сучасний C++.

Сучасний варіант без макросів:

constexpr int kTableWidth = 60;
static_assert(kTableWidth >= 20, "kTableWidth замалий");

Перевага такого підходу в тому, що kTableWidth — нормальна константа мови, а не текстова підстановка.

7. Як читати помилки static_assert

Помилка static_assert зазвичай виглядає страшнішою, ніж є насправді, особливо якщо ви користуєтеся IDE і вона показує ціле «простирадло». Але логіка читання доволі проста: компілятор скаже, що static assertion failed, і поруч буде ваше повідомлення, а також файл і рядок.

Це чи не найдружніша помилка компіляції: у ній уже є людський текст, який ви самі написали для себе. Саме тому варто ще раз повторити: пишіть повідомлення. Без нього ви отримаєте «static assertion failed» і гратимете в гру «вгадай, яка з десяти перевірок спрацювала». З повідомленням ви одразу бачите, яке правило порушено, і зазвичай відразу розумієте, де це виправити.

8. static_assert у шаблонах: мʼяке знайомство

У назві лекції чесно сказано «зокрема для шаблонів». Повноцінно шаблони у нас будуть пізніше, тому тут — лише обережна демонстрація ідеї.

Сенс такий: коли ви пишете шаблонний код «для будь-яких типів», компілятор може спробувати підставити туди тип, який узагалі не підходить. І тоді замість зрозумілої помилки можна отримати легендарне «простирадло» на три екрани. static_assert усередині шаблону дозволяє зупинити компіляцію раніше і сказати людською мовою: «цей шаблон працює лише для таких-то типів».

Нижче — приклад-ескіз. У ньому використано <type_traits> — стандартну бібліотеку для питань на кшталт «це ціле число чи ні». Деталі ми розберемо потім; зараз важливий сам принцип.

#include <type_traits>

template <typename T>
T AddOne(T x) {
    static_assert(std::is_integral_v<T>, "AddOne працює лише для цілих типів");
    return x + 1;
}

Навіть якщо ви поки не до кінця розумієте, що таке template <typename T>, ви можете прочитати цю перевірку як контракт: «функція AddOne дозволена лише для цілих типів». Якщо хтось спробує викликати AddOne(3.14), компілятор не буде мовчки «щось там» підбирати — він покаже вашим повідомленням, що тип не підходить.

Головне, що потрібно винести з цього розділу: static_assert — це спосіб зробити помилки компіляції зрозумілими людині, особливо там, де код стає узагальненим.

9. Де саме спрацьовує static_assert: схема

Щоб остаточно закріпити, «коли» і «де» все відбувається, корисно побачити це як процес. Ось проста блок-схема на рівні ідеї, без командного рядка:

flowchart TD
    A[Початкові файли .cpp/.hpp] --> B[Компіляція]
    B -->|static_assert перевіряється тут| C{Умови істинні?}
    C -->|ні| D[Помилка компіляції; програма не збирається]
    C -->|так| E[Обʼєктні файли .o/.obj]
    E --> F[Лінкування]
    F --> G[Запуск програми]
    G -->|assert перевіряється тут| H{Умови assert істинні?}
    H -->|ні| I[Аварійне завершення]
    H -->|так| J[Нормальна робота]

Ця схема добре підкреслює різницю: static_assert узагалі не дає вам дійти до запуску програми.

10. Типові помилки під час роботи зі static_assert

Помилка № 1: спроба перевірити значення, відоме лише під час виконання, через static_assert.
Найчастіший сценарій — бажання перевірити введення користувача або результат обчислення. Але static_assert живе у світі компілятора й не знає, що введе користувач. Якщо умова залежить від std::cin, часу, файла або мережі, це вже зона if чи assert (залежно від сенсу), але не перевірка під час компіляції.

Помилка № 2: відсутність повідомлення в другому аргументі.
Формально можна писати static_assert(cond);, але тоді в разі спрацювання ви отримаєте «static assertion failed» без пояснення, яке саме правило ви перевіряли. На маленькому навчальному файлі це ще терпимо, а в проєкті перетворюється на «квест». Повідомлення має коротко формулювати контракт: що саме зобовʼязане бути істинним і чому.

Помилка № 3: надто загальна, беззмістовна перевірка.
Іноді пишуть щось на кшталт static_assert(kX != 123); «про всяк випадок». Такий static_assert не пояснює сенсу, і вже завтра ви не памʼятатимете, чому 123 було заборонено. Хороша перевірка описує властивість, а не конкретне магічне число: «має бути додатним», «має бути парним», «має бути не меншим за ширину заголовка».

Помилка № 4: плутанина static_assert із #if і макросами.
Коли ви тягнетеся до макросів, щоб перевірити константи, часто втрачаєте типову безпеку й область видимості. static_assert кращий тим, що перевіряє саме C++-вираз. Макроси залишайте для справді препроцесорних завдань, а контракти, що перевіряються під час компіляції, формулюйте через constexpr + static_assert.

Помилка № 5: спроба «лікувати» логіку програми за допомогою static_assert.
static_assert не замінює алгоритми, перевірку меж під час виконання та нормальну обробку помилок введення. Він корисний для констант, конфігурації й контрактів, які мають бути істинними завжди. Якщо проблема — у неправильному індексі вектора, який зʼявився внаслідок обчислень, вам однаково знадобляться або assert, або акуратні if-перевірки, або правильна логіка обходу.

1
Опитування
Санітайзери, рівень 35, лекція 4
Недоступний
Санітайзери
Санітайзери
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ