1. Компиляция и выполнение: два «мира» программы
Прежде чем произнести магическое слово constexpr, полезно чуть-чуть “раздвоиться” и посмотреть на программу глазами двух персонажей: компилятора и процессора. Компилятор видит ваш код заранее и пытается превратить его в исполняемую программу. Процессор же видит только готовый результат и выполняет инструкции уже во время запуска. И вот constexpr — это способ сказать компилятору: “Эй, дружище, давай вот это посчитай заранее”.
Представьте рецепт и готовку. Рецепт (код) можно прочитать заранее и даже прикинуть калории блюда, не включая плиту. Но чтобы попробовать вкус — нужно реально приготовить. Так же и тут: часть вычислений можно сделать “по рецепту” при компиляции, а часть — только “на кухне” во время выполнения.
Небольшая схема для фиксации:
flowchart TD
A[Исходный код .cpp] --> B[Компиляция]
B --> C[Готовая программа]
C --> D[Запуск]
D --> E[Выполнение на CPU]
B -->|можно вычислить заранее| F[compile-time значения]
D -->|вычисляется при запуске| G[runtime значения]
Идея лекции очень простая: 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. Тогда код будет читаться как текст, а не как ребус.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ