constexpr, consteval and constinit

constexpr, consteval and constinit

Hello, Habre!

Today we will talk about how constexpr, constevaland constinit allow to implement compilation at runtime. p align=”justify”> Runtime compilation allows you to speed up code execution by performing calculations at compile time instead of at runtime.

constexpr enables variable values ​​to be calculated at compile time. Functions and variables declared with this keyword can be evaluated at compile time

consteval reinforces the concept constexprrequiring that expressions be evaluated at compile time.

constinit is used to initialize static and global variables.

And now in more detail.

constexpr

constexpr allows you to define variables or functions in such a way that their the value or result may be computed at compile time. That is, you can perform calculations before the code is executed and thereby save precious processor cycles in runtime.

But not everything is so rosy. constexpr has limitations. Example, it cannot be used with dynamic memory allocation. So what if you need to create constexpr a vector that dynamically changes during compilation will not work.

In addition, functions must be simple enough that the compiler can determine their result at compile time. They also cannot contain some operators such as gotoand cannot call functions that are not constexpr.

Examples

constexpr in functions:

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

int main() {
    // значение будет вычислено во время компиляции.
    constexpr int fact_of_5 = factorial(5);
}

WITH constexpr the compiler calculatesfactorial(5) at compile time and uses this value as a compile-time constant. That is, we get zero runtime overhead for calculating the factorial of 5 when the program starts.

constexpr with classes:

class Point {
public:
    constexpr Point(double x, double y) : x_(x), y_(y) {}

    constexpr double getX() const { return x_; }
    constexpr double getY() const { return y_; }

private:
    double x_;
    double y_;
};

int main() {
    constexpr Point p(10.5, 20.5);
    static_assert(p.getX() == 10.5, "X coordinate should be 10.5");
    static_assert(p.getY() == 20.5, "Y coordinate should be 20.5");
}

Class Point uses constexpr expressions, this allows you to define points with fixed coordinates at compile time and use them without incurring runtime costs.

Limitation constexpr:

#include <iostream>
#include <vector>

constexpr std::vector<int> makeVector(int size) { // ошибка компиляции!
    std::vector<int> v(size, 0);
    return v;
}

int main() {
    auto v = makeVector(5);
}

Usingconstexpr with std::vector will result in a compilation error because std::vector requires dynamic memory allocation, which is not possible in constexpr functions.

consteval

Difference consteval from his brother constexpr in that constexpr offers a choice: if something can be calculated at the compilation stage, fine, but if not – well, let’s try it at runtime. consteval stands for itself: if we cannot calculate it here and now (at the compilation stage), then this expression should not be in the program either.

It’s a masthead when you want to guarantee that certain computations will be done at compile time, excluding any uncertainty and potential runtime overhead. consteval comes in handy when you want to be 100% sure that your code won’t spend extra time calculating at runtime.

example

#include <iostream>

// consteval гарантирует, что функция fibonacci будет вычислена на этапе компиляции
consteval int fibonacci(int n) {
    return (n <= 1) ? n : fibonacci(n-1) + fibonacci(n-2);
}

// использование consteval для инициализации константы на этапе компиляции
constexpr int fib10 = fibonacci(10);

int main() {
    // поскольку fib10 вычисляется на этапе компиляции, здесь нет никаких рантайм вычислений.
    std::cout << "Fibonacci(10) = " << fib10 << std::endl;

    // это также работает:
    // constexpr int fib20 = fibonacci(20);
    // std::cout << "Fibonacci(20) = " << fib20 << std::endl;

    // однако, следующий код не скомпилируется, поскольку значение не может быть вычислено на этапе компиляции
    // int n;
    // std::cin >> n;
    // std::cout << "Fibonacci(n) = " << fibonacci(n) << std::endl;

    return 0;
}

fibonacci with consteval forces the compiler to calculate it at compile time. Results for fibonacci(10) will be built directly into the code being executed as a constant, without the need to enumerate them every time the program is executed.

constant

Unlike constexprwhich is of a kind always calculable expression, and constevalWhich requires computation at compile time without exception, constinit approaches the case more flexibly.

constinit indicates that the variable must be initialized at program start before entry into main(). constinit provides initialization of static or streaming storage without dynamic initialization. In simple language, constinit ensures that the variable is initialized during the program’s loading phase, before the program even begins its execution. From this it follows that, in contrast constexpr, constinit does not require the variable to remain unchanged after initialization.

Suppose there is a global variable that needs to be initialized, but the overhead of dynamic initialization needs to be avoided. Let’s take, for example, the configuration of logging, which should be available immediately at the start of the program:

#include <iostream>
#include <string>

struct LoggerConfig {
    int logLevel;
    std::string logPath;
};

// constinit указывает, что инициализация должна произойти на этапе старта программы.
constinit LoggerConfig globalLoggerConfig{3, "/var/log/myapp.log"};

int main() {
    // при запуске программы globalLoggerConfig уже инициализирован.
    std::cout << "Log Level: " << globalLoggerConfig.logLevel << std::endl;
    std::cout << "Log Path: " << globalLoggerConfig.logPath << std::endl;

    // так как это constinit, мы можем изменять значения после инициализации.
    globalLoggerConfig.logLevel = 4; // Допустимо

    std::cout << "Updated Log Level: " << globalLoggerConfig.logLevel << std::endl;

    // однако, следующий код не скомпилируется, если globalLoggerConfig был объявлен как constexpr
    // constexpr LoggerConfig testConfig{1, "/test.log"};
    // testConfig.logLevel = 2; // ошибка компиляции, т.к. constexpr не допускает изменений после инициализации.

    return 0;
}

It is important to use these tools wisely, combined with an understanding of their features and limitations, in order to maximize the potential of C++ and achieve high performance.

You can learn more about these tools and more at the “C++ Developer” specialization.

Related posts