1. Компіляція та виконання: два «світи» програми
Перш ніж вимовити магічне слово constexpr, корисно трохи «роздвоїтися» й подивитися на програму очима двох персонажів: компілятора та процесора. Компілятор бачить ваш код наперед і намагається перетворити його на виконувану програму. Процесор же бачить лише готовий результат і виконує інструкції вже під час запуску. Отже, constexpr — це спосіб сказати компілятору: «Гей, давай оце порахуй заздалегідь».
Уявіть рецепт і приготування. Рецепт (код) можна прочитати заздалегідь і навіть оцінити калорійність страви, не вмикаючи плиту. Але щоб відчути смак, страву треба справді приготувати. Так само і тут: частину обчислень можна зробити «за рецептом» під час компіляції, а частину — лише «на кухні», уже під час виконання.
Невеличка схема для закріплення:
flowchart TD
A[Вихідний код .cpp] --> B[Компіляція]
B --> C[Готова програма]
C --> D[Запуск]
D --> E[Виконання на процесорі]
B -->|можна обчислити заздалегідь| F[значення під час компіляції]
D -->|обчислюється під час запуску| G[значення під час виконання]
Ідея лекції дуже проста: constexpr стосується значень compile time, тобто тих, які компілятор зобовʼязаний обчислити під час компіляції.
constexpr і const: це не одне й те саме
Якщо ви вже звикли до const, поява constexpr може виглядати як «друга кнопка „не чіпати“ поруч із першою». Але зміст тут інший. const каже: «після ініціалізації змінювати не можна». constexpr каже: «значення має бути відоме під час компіляції».
Це схоже на різницю між «я обіцяю не змінювати пароль після того, як його вигадав» і «пароль має бути відомий заздалегідь і надрукований на папірці ще до того, як ви зайшли в кімнату». Перший варіант допускає, що ви вигадали пароль уже всередині кімнати, тобто під час виконання. Другий вимагає, щоб пароль існував до входу, тобто під час компіляції.
Важливо памʼятати: constexpr — не про швидкість, хоча він часто допомагає з оптимізацією. Передусім це вимога до обчислюваності. У стандарті C++ дуже багато сутностей зі словом constexpr: наприклад, у бібліотеці дедалі більше функцій і операцій роблять доступними для обчислень на етапі компіляції.
Зручна «табличка в голові» (саме в голові; друкувати її на лобі не обовʼязково):
| Ключове слово | Головна думка | Коли відоме значення |
|---|---|---|
|
«Не можна змінювати після ініціалізації» | Може бути відоме лише під час виконання |
|
«Має бути обчислено компілятором» | Має бути відоме на етапі компіляції |
2. constexpr: змінні та вирази
Почнімо з найкориснішого для новачків: constexpr-змінних. Зазвичай це всілякі «магічні числа», яким ви нарешті даєте людське імʼя. Чому не просто const? Тому що іноді вам потрібна гарантія, що це значення справді «вшите в програму», а не отримане десь по дорозі.
constexpr-змінні: «константи, які компілятор зобовʼязаний знати»
Синтаксис виглядає так само звично, як і const:
#include <iostream>
int main() {
constexpr int seconds_per_minute = 60;
constexpr int seconds_per_hour = 60 * seconds_per_minute;
std::cout << seconds_per_hour << '\n'; // 3600
}
Тут компілятор може обчислити все заздалегідь: числа, множення й підсумок. Жодних сюрпризів.
Зауважте важливу деталь: constexpr-змінна майже завжди виглядає як «налаштування» або «константа», а не як «звичайна змінна». Її не змінюють, не збільшують у циклі й не заповнюють через введення. Це радше «вбудоване правило світу» вашої програми.
Ще один приклад із double: так, constexpr працює і для дійсних чисел, якщо вираз можна обчислити:
#include <iostream>
int main() {
constexpr double pi = 3.1415926535;
constexpr double circle = 2.0 * pi;
std::cout << circle << '\n'; // 6.28319...
}
Зараз точність double для нас неважлива — це тема окремої розмови. Нам важливе інше: компілятор здатний зберігати й використовувати такі значення як константи.
Чому constexpr не можна отримати зі std::cin
Дуже поширена помилка в міркуваннях новачків звучить так: «Ну я ж не змінюю значення — отже, можна constexpr». Але constexpr — не про «не змінюю», а про «відомо заздалегідь».
Введення користувача відбувається після запуску програми. На етапі компіляції ще немає ні користувача, ні консолі, ні вашого введення. Компілятор не вміє вгадувати, що саме ви введете. І це добре, бо інакше це був би дуже підозрілий компілятор.
Порівняймо const і constexpr на одному прикладі:
#include <iostream>
int main() {
int x{};
std::cin >> x;
const int a{x}; // можна: фіксуємо значення після введення
constexpr int b{x}; // не можна: x невідомий під час компіляції
std::cout << a << '\n';
}
Тут const цілком доречний: це «знімок» значення після введення, який далі не змінюється. А constexpr не підійде, бо його сенс такий: «це число має існувати ще до запуску».
Якщо сказати зовсім по-людськи: const — це «не чіпай», а constexpr — це «знай заздалегідь».
Константні вирази: що компілятор уміє обчислити заздалегідь
Коли ви пишете constexpr int x = ...;, праворуч від = має бути константний вираз (constant expression). Для нашої базової моделі достатньо вважати, що компілятор добре дає раду «шкільній математиці» над літералами та вже відомими constexpr.
Тобто нормально працюють додавання, віднімання, множення, ділення, дужки й використання інших constexpr-змінних. А от усе, що залежить від зовнішнього світу, наприклад введення, поточний час або читання файлу, на етапі компіляції недоступне.
Подивімося на «хороші» вирази:
#include <iostream>
int main() {
constexpr int width = 80;
constexpr int height = 25;
constexpr int area = width * height;
std::cout << area << '\n'; // 2000
}
А тепер — на приклад, де «майже схоже, але ні»:
#include <iostream>
int main() {
int width{};
std::cin >> width;
constexpr int height = 25;
constexpr int area = width * height; // не можна: width не constexpr
std::cout << width << '\n';
}
Найцінніше тут — не завчити «що можна, а що не можна» як список заклинань, а втримати сенс: вираз compile time не може залежати від даних runtime.
3. Практика: де constexpr допомагає
Щоб тема не зависла у вакуумі «ідеальної математики», продовжимо наш проєкт: маленька консольна програма, яка обчислює підсумкову суму замовлення. Раніше, у темах про типи, арифметику, ініціалізацію та const, ми написали б щось на кшталт «введи суму — отримай суму з податком».
Тепер додамо constexpr там, де значення справді «вшите» у правила програми: наприклад, ставка податку та сервісний збір.
Міні-застосунок «Чек у кафе»
Уявімо, що в нашому кафе податок становить 10 %, а сервісний збір — 5 %. Це не введення користувача й не змінне значення, а правило, яке ви зафіксували в коді.
#include <iostream>
int main() {
constexpr double tax_rate = 0.10;
constexpr double service_rate = 0.05;
double subtotal{};
std::cin >> subtotal;
const double total = subtotal * (1.0 + tax_rate + service_rate);
std::cout << total << '\n';
}
Тут tax_rate і service_rate — ідеальні кандидати на constexpr. А от subtotal — ні, бо це введення.
Зверніть увагу на приємний ефект: формулу стало легше читати. Замість «магії» subtotal * 1.15 ви явно показуєте, звідки взялися 15 %. Коли через тиждень ви відкриєте цей код, вам не доведеться гадати, що означає 1.15 і чому не 1.12.
Тепер — контрастний сценарій: «ставка податку вводиться користувачем» — наприклад, тому що ми тестуємо різні режими. У цьому разі constexpr за змістом не підходить, але const може бути корисним як «знімок налаштувань».
#include <iostream>
int main() {
double subtotal{};
double tax_rate{};
std::cin >> subtotal >> tax_rate;
const double snapshot_tax{tax_rate}; // фіксуємо, щоб випадково не змінити
const double total = subtotal * (1.0 + snapshot_tax);
std::cout << total << '\n';
}
Це хороший момент, щоб чітко зафіксувати думку: constexpr — не «найкрутіший const». Він не має витісняти const. Вони відповідають на різні запитання.
Як обрати між const і constexpr
Коли ви починаєте використовувати const і constexpr, легко виникає бажання все класифікувати: «О, це точно constexpr, а це… здається… const?». Щоб не потонути в сумнівах, досить двох простих запитань. Їх варто ставити собі щоразу, коли ви бачите в коді «константне» значення.
Перше запитання звучить так: «Це значення за змістом має змінюватися?». Якщо так, це звичайна змінна — без const і без constexpr. Якщо ні, рухаємося далі.
Друге запитання: «Це значення зобовʼязане бути відомим під час компіляції?». Якщо так — constexpr. Якщо ні, але змінювати його все одно не можна, — const.
Ця логіка виглядає так:
flowchart TD
A[Потрібне значення X] --> B{X має змінюватися?}
B -- так --> C[Звичайна змінна]
B -- ні --> D{X має бути відомим під час компіляції?}
D -- так --> E[constexpr]
D -- ні --> F[const]
І дуже важлива ремарка: на рівні нашої лекції «відомо під час компіляції» найчастіше означає «це чиста математика над літералами та іншими constexpr». Усе.
4. Типові помилки під час роботи з constexpr
Помилка № 1: намагатися зробити constexpr із даних, уведених користувачем.
Це трапляється майже з усіма: «Я ж не змінюю значення, чому не можна?». Не можна, бо користувач вводить дані після запуску програми, а constexpr — це вимога обчислюваності ще до запуску. Якщо значення надійшло зі std::cin, воно максимум може стати const, але не constexpr.
Помилка № 2: вважати, що constexpr — це просто «прискорювач».
Іноді constexpr справді дозволяє компілятору виконати більше оптимізацій, але головний сенс не в цьому. Головний сенс — у гарантії: якщо ви оголосили constexpr, а вираз насправді не обчислюється на етапі компіляції, компілятор вас зупинить. Це більше схоже на «страховку від хибної ідеї», ніж на «турборежим».
Помилка № 3: ставити constexpr на все підряд «для краси».
Коли ключове слово стає прикрасою, воно перестає бути сигналом. constexpr варто використовувати там, де воно справді відображає контракт: «ця штука — частина правил програми, вона фіксована й відома заздалегідь». Якщо значення залежить від користувача, конфігурації чи введення, не треба намагатися «продавити» його в constexpr.
Помилка № 4: забути, що constexpr (як і const) вимагає ініціалізації одразу.
Не можна сказати «я оголошу constexpr int x;», а потім уже вигадаю значення. У цьому немає сенсу: якщо компілятор має знати значення заздалегідь, ви повинні дати його одразу під час оголошення. На практиці це лікується звичкою акуратно ініціалізувати змінні. І так, порожні {} теж допомагають, але для constexpr все одно потрібне конкретне значення.
Помилка № 5: залишати «магічні числа» без імені, навіть коли вони очевидно constexpr.
Іноді пишуть subtotal * 1.15 і думають: «Та й так зрозуміло». Через місяць зазвичай уже не зрозуміло. Якщо число — частина логіки, наприклад податок, ліміт чи коефіцієнт, дайте йому імʼя через constexpr. Тоді код читатиметься як текст, а не як ребус.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ