-1

The toy problem of recursive (non-caching) fibonacci can be implemented as following:

#include <iostream>

int fibonacci(int n) {
    if (n <= 1)
        return n;
    else
        return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    int N = 5;
    int result = fibonacci(N);
    std::cout << "Fibonacci(" << N << ") = " << result << std::endl;
    return 0;
}

The output of this program is Fibonacci(5) = 5. With metaprogramming, this can be evaluated during compilation:

#include <iostream>

consteval int fibonacci(int n) {
    if (n <= 1)
        return n;
    else
        return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    constexpr int N = 10;
    constexpr int result = fibonacci(N);
    std::cout << "Fibonacci(" << N << ") = " << result << std::endl;
    return 0;
}

But why is it necessary to make the program more verbose? Couldn't the compiler analyze the first program, and figure out that the output is always 5?

user23952
  • 578
  • 3
  • 10
  • 1
    It could up to a compiler. – 273K Apr 22 '23 at 17:24
  • 2
    "Doesn't" and "Couldn't" are two different things. – Nicol Bolas Apr 22 '23 at 17:25
  • 1
    Compilers often do inline and constant-propagate through functions at compile time, even without `consteval` to force it. But they're not forced to, so it doesn't happen in debug builds. And surprisingly in this case, it doesn't happen for GCC or clang even for N=5. https://godbolt.org/z/6j7T1c5WY . I guess the default heuristics for inlining recursive functions even at `-O3` are reluctant to go that far, although GCC `-O3` does balloon `fibonacci` to pretty large code size, probably multiple levels of recursive inlining. But not far enough to get to the base case for N=3 or higher. – Peter Cordes Apr 22 '23 at 17:26
  • Also, the C++ committee wants programs to be valid or invalid according to the standard, not depending on how well a given compiler is able to optimize. If you want to use `int arr[foo(N)]`, you need a *guarantee* that the return value is a constant expression. The fact that some compilers would be able to resolve `foo(N)` to a compile time constant while others couldn't would be a problem. Or even the same compiler in a non-optimizing build; code that can only be compiled with optimization enabled is not good. (Actually just `constexpr` is all we need for that.) – Peter Cordes Apr 22 '23 at 17:35
  • 6
    I don't think this question needed to be closed, certainly not as opinion-based. There are reasons for `consteval` to exist as a language feature, to tell compilers that doing constant-propagation through some code is worth it because it will result in a compile-time constant if it continues long enough. Usually compilers don't know that so they bail out after some heuristic limits. (Halting problem...) The question might be a duplicate if one already exists, and with some research effort you could have found out that two mainstream compilers (gcc/clang) don't do the opt you hoped for. – Peter Cordes Apr 22 '23 at 17:41
  • @PeterCordes: "*to tell compilers that doing constant-propagation through some code is worth it because it will result in a compile-time constant if it continues long enough*" That's absolutely *not* what `consteval` is for. `consteval`, and `constexpr` for that matter, are not tools for directing compiler optimizations (at least, not the way that you mean it). – Nicol Bolas Apr 22 '23 at 18:34
  • @PeterCordes: "*I don't think this question needed to be closed, certainly not as opinion-based.*" Unless you are intimately familiar with the internals of a particular compiler, you cannot *know* why it didn't optimize this code. You can only speculate. – Nicol Bolas Apr 22 '23 at 18:37
  • @NicolBolas: That's not what this question is asking, though. It seems to be asking why you'd ever use `consteval` ("what is this language feature for?"), not *why* any compilers fail to constant-propagate through this `fibonacci` function. (The question doesn't even get as far as checking whether any mainstream compilers do constant-propagate or not.) – Peter Cordes Apr 22 '23 at 18:39
  • 1
    @PeterCordes: "*not why any compilers fail to constant-propagate through this fibonacci function*" How else do you interpret this sentence: "But why is it necessary to make the program more verbose?" The "more verbose" is referring to the addition of `constexpr` and `consteval`. Also, there's the "Couldn't the compiler analyze the first program, and figure out that the output is always 5?" part, which again, makes it clear that the question is why the compiler isn't doing this at compile-time without explicitly saying to do so. – Nicol Bolas Apr 22 '23 at 18:41
  • 1
    A decent implementation of fibonacci is optimized out, even if you don't add `constexpr`/`consteval`, see https://godbolt.org/z/o6PP51dfa ; If you're wondering, if `inline` makes a difference: It doesn't; the `main` function is the same even without it. It's simply a convenient way of dropping that function from the compiler output... – fabian Apr 22 '23 at 19:04
  • @NIcalBolas "But why is it necessary ..." assumes that compiler *cannot* possibly evalute the first version at compile time. The rest is just based on this false premise. That a compiler can potentially do it is not about opinions – 463035818_is_not_an_ai Apr 22 '23 at 19:32
  • @PeterCordes thanks for a thorough explanation of `constexpr`/`consteval` and their role in the C++ language. I was looking for a way to force the compiler to constant propagate an expression during compile time or die trying, but as you mention, the correctness of the program would then depend on the cleverness of the compiler, so it cannot be a feature of the language itself. For recursive Fibonacci, it would be unreasonable to expect the compiler to figure this out on its own when the value of the input is large. I was however surprised that it didn't happen for any value of the input. – user23952 Apr 23 '23 at 10:39
  • 1
    Your question has nothing to do with metaprogramming, you are not using templates. Constexpr/consteval keywords are not metaprogamming unless used as template arguments. – Vladislav Kogan Apr 24 '23 at 01:03
  • @VladislavKogan it is according to ChatGPT: "Yes, consteval is a form of metaprogramming in C++. Metaprogramming is the practice of writing code that generates other code at compile-time. It is a powerful technique that can lead to more efficient and flexible programs." – user23952 Apr 24 '23 at 08:31
  • Constexpr does not generate **other** code, it's the **same** code that could be evaluated at compile time. ChatGPT is not a reliable source, don't trust & double-check anything it says. Read any human-written material about metaprogramming in C++ – https://stackoverflow.com/questions/980492/what-is-metaprogramming – Vladislav Kogan Apr 24 '23 at 13:02
  • It couldn't always be evaluated at compile time, as for example `int arr[foo(N)]` from @PeterCordes's answer. – user23952 Apr 24 '23 at 19:39
  • 1
    I didn’t said the opposite. And again, that’s still have nothing to do with metaprogramming. – Vladislav Kogan Apr 24 '23 at 20:48

1 Answers1

3

Compilers often do inline and constant-propagate through functions at compile time, even without consteval to force it. But they're not forced to, so it doesn't happen in unoptimized debug builds.

And surprisingly in this case, it doesn't happen for GCC or clang even for N=3 or higher. https://godbolt.org/z/6j7T1c5WY

I guess the default heuristics for inlining recursive functions even at -O3 are reluctant to go far enough, although GCC -O3 does balloon fibonacci to pretty large code size. GCC and clang convert one of the recursions into looping, but GCC goes farther. I'm not sure what exactly it's doing with all that code, and why it doesn't evaluate fibonacci(3) to a compile-time constant without consteval.


Since constexpr exists as a way to let programmers write programs that do stuff like int arr[foo(N)], it's somewhat natural to extend that to a way to get guaranteed constant evaluation even in contexts where it's not required. (e.g. something other than an array dimension or template parameter.)

Where previously a programmer would have had to use template metaprogramming to be guaranteed that there was no runtime overhead for something they wanted to compute, consteval lets them use normal code, taking advantage of the same compiler features that constexpr depends on, including in debug builds.

constexpr itself exists because the C++ committee wants programs to be valid or invalid according to the standard, not depending on how well a given compiler is able to optimize. If you want to use int arr[foo(N)], you need a guarantee that the return value is a constant expression. The fact that some compilers would be able to resolve foo(N) to a compile time constant while others couldn't would be a problem. Or even the same compiler in a non-optimizing build; code that can only be compiled with optimization enabled is not good.


So why use consteval? Do you want a guaranteed constant expression, or are you happy with just checking that some compiler you care about is able to optimize well in your use-case, when optimization is enabled? Often the latter is sufficient for most use-cases.

It's a way to tell compilers that doing constant-propagation through some code will definitely result in a compile-time constant if it continues long enough. Usually compilers don't know that so they bail out after some heuristic limits.

(There are lots of use-cases for constexpr other than performance, but I'm less sure about consteval.)

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847