GCC and Clang are behaving differently with respect to constant evaluation
Asked Answered
T

1

2

I'm observing an inconsistency between GCC and Clang with respect to what is a constant evaluated context. I played with different situations:

#include <iostream>
#include <type_traits>

consteval auto ceval(auto x) { return x * x; }

constexpr auto constexprif(auto x) {
    if constexpr (std::is_integral_v<decltype(x)>) {
        return ceval(x);
    } else {
        return x + 2.;
    }
}
constexpr auto constexprif_consteval(auto x) {
    if constexpr (std::is_integral_v<decltype(x)>) {
        if consteval {
            return ceval(x);
        } else {
            return x * x + 1;
        }
    } else {
        return x + 2.;
    }
}

constexpr auto constevalif(auto x) {
    if consteval {
        return ceval(x);
    } else {
        return static_cast<decltype(x)>(x + 2.);
    }
}

constexpr auto isconstantevaluated(auto x) {
    if (std::is_constant_evaluated()) {
        return ceval(x);
    } else {
        return static_cast<decltype(x)>(x + 2.);
    }
}

int main() {
#ifdef __clang__  // GCC KO
    auto a = constexprif(42);
    const auto b = constexprif(42);
    constexpr auto c = constexprif(42);
    std::cout << "constexprif " << a << '\t' << b << '\t' << c << '\n';
#endif
    auto d = constexprif_consteval(42);
    const auto e = constexprif_consteval(42);
    constexpr auto f = constexprif_consteval(42);
    std::cout << "constexprif_consteval " << d << '\t' << e << '\t' << f
              << '\n';
    auto g = constevalif(42);
    const auto h = constevalif(42);
    constexpr auto i = constevalif(42);
    std::cout << "constevalif " << g << '\t' << h << '\t' << i << '\n';
// auto j = isconstantevaluated(42);
#ifdef __clang__  // GCC KO
    const auto k = isconstantevaluated(42);
    constexpr auto l = isconstantevaluated(42);
    std::cout << "isconstantevaluated " << k << '\t' << l << '\n';
#endif
}

Live

ceval is a basic consteval function, just to see in what context it can be called by a constexpr function.

constexprif is a function that (incorrectly) tried to return ceval if its argument is integral. Obviously, calling it with a runtime argument will not compile.

constexprif_consteval is a fixed version that will call cevalonly in a constant evaluate context.

constevalif calls ceval in a constant evaluate context and should have the same output as constexprif_consteval for integral arguments.

Eventually, isconstantevaluated is the same as constevalif but with a if (std::is_constant_evaluated()) instead of an if consteval.

The output is as follows.

For GCC:

constexprif_consteval 1765    1764    1764
constevalif 44    1764    1764

For Clang:

constexprif 1764    1764    1764
constexprif_consteval 1764    1764    1764
constevalif 1764    1764    1764
isconstantevaluated 1764    1764

I took note of this post about a Clang bug, but it's quite old now and I think that this question is going a bit deeper.

My expectations are the following.
I think that GCC rightfully refuses constexprif in all situations because instantiating the function for an integral type may lead to use it in a runtime context.
Clang seems overzealous to accept it in all cases, only on the basis that the argument is known at compile time.

With constexprif_consteval GCC seems to use the context (a non-const initialization) to chose the not consteval branch while Clang also goes for the full compile-time branch.

With constevalif we're observing the same behavior.

Eventually isconstantevaluated is always rejected by GCC, the if (std::is_constant_evaluated()) being a runtime test.

Thus my impression is that GCC is correct with respect to my understanding of the standard so far. Is it so? (yet, IMHO, the clang behavior seems easier to understand for me: arguments are known at compile-time? then the expression can be constant evaluated).

Thyratron answered 26/2 at 15:31 Comment(2)
It might be beneficial to add/tweak tags so that language-lawyer is included.Rheumatoid
Not read into it, but might be to do with P2564R3, which GCC claims to support in GCC14 and Clang in clang17. Sure enough this doesn't compile on your godbolt link when you change from gcc13 -> gcc trunkTwobit
S
1

Before P2564 which was accepted for C++23, the situation was as follows:

ceval(x) is an invocation of an immediate function (i.e. a function marked consteval). Every invocation of an immediate function that is not in an immediate function context, i.e. either in the body of another consteval function or in the compound statement of a if consteval, is a so-called immediate invocation and must itself be a constant expression.

As you are saying, because x is not usable in constant expressions and its lifetime began outside of ceval(x), ceval(x) itself can never be a constant expression.

As a result all of your attempts which use if consteval should succeed and all others should fail, as GCC is showing in your demonstration.

Whether the if consteval branch is taken depends on whether the context is manifestly constant evaluated. That's the case for the initializations of f and i because they are marked constexpr and for e and h because they are const integral type variables initialized by constant expressions (which makes them usable in constant expressions as well).

However, d and g are not usable in constant expressions, because they are neither of the two categories mentioned above. Furthermore they are not static storage duration and therefore their initialization is not manifestly constant-evaluated. In these cases the runtime evaluation should be used.

GCC in your demonstration seems to implement this correctly.


With P2564 the notion of "immediate-escalating" expressions and functions was introduced to propagate the constant evaluation of consteval functions upwards.

Then in the cases without if consteval, because ceval(x) is an immediate invocation that is not a constant expression and not in an immediate function context, it is an immediate-escalating expression.

A specialization of a function template marked constexpr is also an immediate-escalating function.

Then, because this immediate-escalating function contains the immediate-escalating expression (directly in its function body without any other intervening non-block scope), the function itself becomes an immediate function, i.e. it will behave as if it is marked consteval.

When the outer function itself is an immediate function, then the call ___(42) itself will be evaluated at compile-time, regardless of the context it is used in and the function body is always manifestly-constant evaluated. So, this works out here and the compile-time path is always chosen. Clang is implementing this correctly.

The only issue I see is with the behavior after P2564 in the cases with if consteval. Because the calls to ceval are in the if consteval body, they are in an immediate function context and therefore can't be immediate-escalating expressions. Consequently I think the behavior in these cases should be unchanged from before P2564. However, Clang seems to incorrectly choose the compile-time path for d and g.

Stoichiometric answered 26/2 at 17:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.