Avatar ·

В чем разница между constexpr и const в C++?

Привет! Я недавно начал изучать C++11 и наткнулся на ключевое слово `constexpr`. Я понимаю, что `const` означает, что значение переменной не может быть изменено после инициализации. Но `constexpr` тоже вроде бы делает переменную константной. В чем тогда разница? Например, я могу написать: ```cpp const int x = 5; constexpr int y = 5; ``` Оба варианта работают. Но я слышал, что `constexpr` можно использовать с функциями. Я попробовал: ```cpp constexpr int square(int n) { return n * n; } int main() { const int a = square(5); // Работает? constexpr int b = square(5); // Тоже работает? int arr[ square(3) ]; // Можно ли так? } ``` Это компилируется, но я не понимаю, в чем реальное преимущество. Когда нужно использовать `constexpr` вместо `const`? И почему просто `const` недостаточно? Мне кажется, что это дублирование функциональности. Объясните, пожалуйста, на примерах, когда что применять. Заранее спасибо! <<>>

Ключевое различие: время вычисления

Главное отличие между const и constexpr в C++ — это когда вычисляется значение. const гарантирует, что переменная не будет изменена после инициализации, но значение может быть вычислено во время выполнения. constexpr требует, чтобы значение было вычислено на этапе компиляции (compile-time), если это возможно.

Проще говоря: const — это "я обещаю не менять эту переменную". constexpr — это "это значение должно быть известно компилятору до запуска программы".

Пример с переменными

int get_value() {
    return 42; // Может быть любым вычислением
}

int main() {
    const int a = get_value();  // ОК: a не изменится, но значение вычисляется в runtime
    // constexpr int b = get_value(); // Ошибка компиляции! get_value() не constexpr
    
    const int c = 5;            // ОК: константа времени компиляции
    constexpr int d = 5;        // ОК: то же самое
}

Функции: самое интересное

С функциями разница становится критической. constexpr функция может быть вычислена на этапе компиляции, если все её аргументы — константы времени компиляции. Если нет — она работает как обычная функция.

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int f5 = factorial(5); // Вычислено на этапе компиляции! f5 = 120
    int x = 5;
    int f_x = factorial(x);          // Вычислено в runtime, но работает
    
    int arr[ factorial(4) ];         // ОК: размер массива известен во время компиляции
    // int arr2[ factorial(x) ];     // Ошибка: x не constexpr
}

Когда использовать что?

  • Используйте const для переменных, которые не будут меняться, но значение может быть получено динамически (например, из пользовательского ввода или вычислений в runtime).
  • Используйте constexpr когда вам нужно гарантировать, что значение известно на этапе компиляции. Это необходимо для:
    • Размеров массивов (в старом C++), шаблонных параметров (например, std::array<int, N>)
    • Оптимизации — константы времени компиляции могут быть встроены в код без вызова функции
    • Метапрограммирования — вычисления во время компиляции сложных значений

Важное замечание про C++17 и C++20

В C++17 constexpr лямбды стали возможны. В C++20 constexpr функции могут содержать try-catch, dynamic_cast и даже виртуальные вызовы (в ограниченных случаях). Также появились constexpr векторы и строки (но с ограничениями).

На практике: если вы пишете функцию, которая может быть вычислена на этапе компиляции, делайте её constexpr. Компилятор сам решит, вызывать её в compile-time или runtime. Для переменных — используйте constexpr если значение точно известно на этапе компиляции, и const в остальных случаях.

<<>>

Дополнение: constexpr в контексте оптимизации

Ещё один важный аспект — constexpr позволяет выполнять "вычисления во время компиляции" (compile-time computation), что может значительно ускорить программу. Например, если у вас есть таблица синусов, которую нужно вычислить один раз, вы можете сделать это на этапе компиляции:

constexpr double sin_table[360] = { /* вычисления */ };

Это особенно полезно для встраиваемых систем или игр, где каждый такт процессора на счету. const такой гибкости не даёт — он лишь запрещает изменение, но не гарантирует вычисление на этапе компиляции.

Также стоит отметить, что constexpr переменные по умолчанию имеют внутреннее связывание (internal linkage), как и const с глобальной областью видимости. Но если вы используете extern constexpr, это вызовет ошибку — constexpr не может быть объявлен с внешним связыванием, потому что его значение должно быть известно в каждой единице трансляции.

На практике: если вы сомневаетесь, используйте const для переменных и constexpr для функций, которые могут быть вычислены на этапе компиляции. Это безопасный и эффективный подход.