Consteval in C++ (ENG)

Make Compile-Time Great Again (Since C++20, Part I)

After learning about the constexpr specifier, we have got some understandings about compile-time optimization. In this note, let's explore the consteval specifier.

The consteval specifier was introduced in C++20 and it declares a function as an "immediate function", meaning that every call to the function must produce a compile-time constant expression. This enforces that the function is always evaluated at compile-time.

You can put constexpr anywhere, but consteval is only allowed in function and function template declarations.

CEFE is a Must

Our question here is: what's the different between constexpr and consteval? We have one main difference though. The constexpr specifier does not provide a guarantee about function evaluation at compile-time, which means the the machine code may vary depending on the compiler. However, consteval can give you a guarantee, ensuring Compile-Time Function Execution(CTFE).

For example, GCC may be strict about CTFE, making every possible constexpr declared function inlined and a constant expression at compile-time. On the other hand, MSVC may be more lenient about CTFE, possibly not performing the work at compile-time. And if the constexpr function do executing at run-time which we would not like to, the compiler won't complain anything about it.

Let's dive into an example we are familiar with:

#include <iostream>

constexpr int add(int a, int b){
    return a + b;
}

int main() {
    int a = 100; // no constexpr declared this time
    int b = 200; // no constexpr declared this time
    int c = add(a, b); // no constexpr declared this time
    return 0;
}

After compilation, let's see what we get:

add(int, int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-8]
        add     eax, edx
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 100
        mov     DWORD PTR [rbp-8], 200
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, edx
        mov     edi, eax
        call    add(int, int)
        mov     DWORD PTR [rbp-12], eax
        mov     eax, 0
        leave
        ret

The add() function is back, which means the function will execute at runtime. This is because the variables a and b are regular variables, while a constant expression evaluation cannot depend on any runtime-modifiable regular variables. Even with constexpr, compiler cannot give you a guarantee about CTFE.

But with consteval, you will get a compiler error instead. This is because the function add is declared consteval, but the compiler cannot determine the function value at compile-time with regular values.

#include <iostream>

consteval int add(int a, int b){
    return a + b;
}

int main() {
	const int a = 100; // constexpr declaration will also work
	const int b = 200; // constexpr declaration will also work
	int c = add(a, b); // okay
	int c2 = add(a, add(a, b)); // okay
	
    int d = 100; // error, must be constant expression
    int e = 200; // error, must be constant expression
    int f = add(d, e); // error, add(a, b) is not a constant expression
    return 0;
}