Why is std::nextafter not constant expression?
Asked Answered
T

3

8

Why code below has no problem with a2 but does not compile for z1?

#include <cmath>    // std::nextafter
#include <limits>   // std::numeric_limits

int main ()
{
    constexpr float a1 {1.f};
    constexpr float a2 {std::nextafter(a1, std::numeric_limits<float>::max())};
    constexpr float z0 {0.f};
    constexpr float z1 {std::nextafter(z0, std::numeric_limits<float>::max())};
    
    return 0;
}

Compiled with GCC 13.2

In file included from <source>:1:
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/cmath: In function 'int main()':
<source>:9:39:   in 'constexpr' expansion of 'std::nextafter(((float)z0), std::numeric_limits<float>::max())'
/opt/compiler-explorer/gcc-13.2.0/include/c++/13.2.0/cmath:2417:32: error: '__builtin_nextafterf(0.0f, 3.40282347e+38f)' is not a constant expression
 2417 |   { return __builtin_nextafterf(__x, __y); }

So GCC compiled a2 correctly but is unable to compile z1.

Note: Clang 14.0 and MSVC 19.38 have problems even with a2.

Therewithal answered 3/3, 2024 at 17:19 Comment(2)
I get errors for both a2 and z1: godbolt.org/z/1cfs5q8TKTowne
godbolt.org/z/9aWMM8efs - no problem with a2Maim
S
6

libstdc++13.2.0 does not seem to implement std::nextafter() as constexpr yet.

GCC turns std::nextafter(a1, std::numeric_limits<float>::max()) into a constant value. This is a lucky exceptional case when compilers can evaluate rare expressions as constexpr even if they are not marked as such.

GCC can't turn std::nextafter(z0, std::numeric_limits<float>::max()) into a constant value, it turns __builtin_nextafterf() into the call to the C function nextafterf() that can't be constexpr. See https://godbolt.org/z/v7KMrTdfW

The code can be simplified to

#include <cmath>    // std::nextafter
#include <limits>   // std::numeric_limits

int main ()
{
    constexpr float a2 {std::nextafter(1.f, std::numeric_limits<float>::max())};
    constexpr float z1 {std::nextafter(0.f, std::numeric_limits<float>::max())};
    return 0;
}

with the similar Assembler result: https://godbolt.org/z/KE9vGd6j7

Servo answered 3/3, 2024 at 20:15 Comment(7)
An interesting thing to note is that it seems GCC only doesn't do the optimization when the first parameter is 0. Any other value I use gets the code the compile.Towne
@Towne It seems to me that it is true. As it cannot make constexpr function godbolt.org/z/v381ordPz but can optimize it away.Maim
@273K a2 is not 1 (see godbolt in comment above) but it can optimize it away as iostream standard precision is 6. But your answer make a point.Maim
@TomášNadrchal You are right, I read the Assembler improperly.Servo
@Towne I think the thing about not const-expressing nextafter(0.) is because the next value is a denormalized float, which could be rounded to zero at runtime. nextafter(std::numeric_limits<float>::min(), …) gets optimized away. nextafter(std::numeric_limits<float>::denorm_min(), …) does notHarar
Compiling with -fno-trapping-math -fno-math-errno also gets rid of the nextafter calls. So I guess it's less about potentially rounding denormalized numbers to zero and more about those two featuresHarar
"This is a lucky exceptional case when compilers can evaluate rare expressions as constexpr even if they are not marked as such.": No, that would be non-conforming to the standard. The compiler is not allowed to accept the constexpr variable initialization if std::nextafter is not marked constexpr. GCC is (intentionally) non-conforming and considers the math functions to be constexpr even if they aren't. In C++23 the second case will fail because the result is subnormal and would raise FE_UNDERFLOW.Jasper
J
4

Since C++23 most C library math functions, including std::nextafter, are marked constexpr and generally usable in constant expression. However, there is an exception to this if the call would set errno or would raise any floating point exception other than FE_INEXACT (according to the semantics of ISO C Annex F as applicable to the floating point types).

This guarantees that floating point exceptions and the global errno that were intended to be observable at runtime don't "vanish" (except for FE_INEXACT which was excluded).

So, if you are asking why z1 doesn't compile in C++23 mode, then it is because given that float on GCC is an IEEE 754 type and the result would be subnormal, ISO C Annex F applies and specifies std::nextafter to raise FE_UNDERFLOW, making the call non-eligible for constant expression evaluation under the exceptions I mentioned above.

If you are asking why a2 does compile with GCC even in C++20 or earlier mode, then it is because GCC even before C++23 handled the C standard library math functions as if they were marked constexpr, although that's technically not conforming to the standard.

According to the standard a2's initialization must produce a diagnostic before C++23, because the initializer calls a function that isn't marked constexpr (and the compiler may not add it implicitly). Calling a non-constexpr function in an expression disqualifies that expression from being a constant expression.

If you are asking why Clang and MSVC don't accept either variable, then it is probably because they haven't implemented the C++23 changes yet.

Jasper answered 4/3, 2024 at 1:1 Comment(9)
although that's technically not conforming to the standard. How so? It was not required to be constexpr but it was not prohibited to be constexpr. Built-in functions are often implicit contsexpr.Servo
@273K Yes it was prohibited. It is a well-known intentional divergence of GCC from the standard: timsong-cpp.github.io/cppwp/n4868/…Jasper
You have quoted that the constexpr keyword shall not be used in any standard library function declaration except if constexpr is explicitly required. 1. __builtin_nextafterf() is not a standard library function. 2. Missing constexpr does not prohibit a function from being used in constant expression context.Servo
@273K OP is using the standard library function std::nextafter, not a compiler built-in. And missing constexpr on a function being called disqualifies from being a constant expression (timsong-cpp.github.io/cppwp/n4868/expr.const#5.2), which in turn, given that the variable being initialized is marked constexpr implies that the program must be ill-formed (with a diagnostic required) per timsong-cpp.github.io/cppwp/n4868/dcl.constexpr#10.sentence-3 (because of the word "shall").Jasper
I don't see there "disqualifies" or "then and only then". I consider the whole paragraph 5 as required but not limited.Servo
@273K timsong-cpp.github.io/cppwp/n4868/expr.const#5 is a proper definition for the term "core constant expression". As a definition it doesn't leave room for anything else being one. And being a "core constant expression" is a necessary condition for being a "constant expression" which is defined later in timsong-cpp.github.io/cppwp/n4868/expr.const#11. The wording "A [italic term] is a [...]" clearly indicates that the sentence is supposed to be a definition of [italic term].Jasper
@273K This is different from C btw. which to some degree explicitly permits implementations to consider expressions to be constant expressions even if the standard doesn't mandate it. In C++ the language fully specifies whether or not an expression is a constant expression (some unresolved defects and implemenation-defined choices aside).Jasper
An entity is a permitted result of a constant expression if ..., or if it is a non-immediate function.Servo
@273K That's the definition for the term "permitted result of a constant expression" which doesn't appear in anything relevant to my previous comments. It is only referenced in the definition of "constant expression" to further restrict it, but that definition already has the necessary condition "A constant expression is either a glvalue core constant expression [...] or a prvalue core constant expression [...]": As explained above, the full-expression of the initialization already fails being a core constant expression, so it doesn't matter what else the definition requires.Jasper
B
1

nextafter was not required to be a constexpr function before C++23. Since that's still a new standard, many standard library implementations have not implemented it yet.

Beauregard answered 3/3, 2024 at 18:42 Comment(3)
I would agree, but why a2 compile and z1 does not? I would expect either both to compile (implemented) or not compile (not implemented).Maim
@TomášNadrchal It is non-conforming behavior that GCC also has in <C++23 modes. GCC is known for having added constexpr specifiers to math functions even though that wasn't permitted by the standard.Jasper
@NicolBolas There is an exception to use in constant expressions if a floating point exception would be raised, so z1 will fail in C++23 as well, see my answer.Jasper

© 2022 - 2025 — McMap. All rights reserved.