JVApen
JVApen

Reputation: 11317

Are floating point operations resulting in infinity undefined behavior for IEC 559/IEEE 754 floating-point types

I was reading through Infinity not constexpr, which seems to indicate that creating infinity is undefined behavior:

[expr]/4:

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined.

However, if std::numeric_limits::is_iec559 equals true, it seems to give us more guarantees.

The code below makes use of this guarantee in order to create an infinite number. When executed in constexpr context, it results in a compiler failure as this is undefined behavior in case is_iec559 equals false.

// clang++ -std=c++17 -O3
#include <limits>

constexpr double createInfinity()
{
    static_assert(std::numeric_limits<double>::is_iec559, "asdf");
    double d = 999999999999;
    while (d != std::numeric_limits<double>::infinity())
    {
        d *= d;
    }
    return -1*d;
}

static_assert(createInfinity() == std::numeric_limits<double>::infinity(), "inf");

Code at Compiler Explorer

As this function always results in infinite, it can never be called in a valid C++ program. However, as we assert on the is_iec559, we get extra guarantees. Is this program still invalid?

(Answers can use both C++17 as the upcoming C++20, please clearly indicate which is used)

Upvotes: 11

Views: 1387

Answers (2)

L. F.
L. F.

Reputation: 20579

The program is ill-formed.

Per [expr.const]/4,

An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:

  • [...]

  • an operation that would have undefined behavior as specified in [intro] through [cpp] of this document [ Note: including, for example, signed integer overflow ([expr.prop]), certain pointer arithmetic ([expr.add]), division by zero, or certain shift operations — end note ];

  • [...]

And [expr.pre]/4 says:

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined. [ Note: Treatment of division by zero, forming a remainder using a zero divisor, and all floating-point exceptions vary among machines, and is sometimes adjustable by a library function. — end note ]

Note that mathematically, the product of two finite numbers is never infinity, so the product triggers undefined behavior. The implementation may specify such expressions as defined, but it is still undefined behavior from the perspective of the standard. The definition by the implementation does not apply retrospectively to other parts of the standard. Therefore, the program is ill-formed because it tries to evaluate an expression that triggers undefined behavior as a constant expression.


It is interesting though, that numeric_limits<double>::infinity() is constexpr. This is fine. Per [numeric.limits]/infinity:

static constexpr T infinity() noexcept;

Representation of positive infinity, if available.

Meaningful for all specializations for which has_­infinity != false. Required in specializations for which is_­iec559 != false.

If is_iec559 == true, then has_infinity == true, and the infinity value is returned. If is_iec559 == false, has_infinity may be true, in which case the infinity value is also returned, or it may be false, in which case infinity() returns 0. (!)

However, since the product of two big numbers is not automatically infinity (it is undefined behavior instead), there is no contradiction. Passing infinity around is fine (the infinity value is always in the range of representable values), but multiplying two big numbers and assuming the result is infinity is not.

Upvotes: 2

JVApen
JVApen

Reputation: 11317

Waiting some time sometimes helps, looks like Clang has received a patch that makes this code compile: https://reviews.llvm.org/D63793

Prior to r329065, we used [-max, max] as the range of representable values because LLVM's fptrunc did not guarantee defined behavior when truncating from a larger floating-point type to a smaller one. Now that has been fixed, we can make clang follow normal IEEE 754 semantics in this regard and take the larger range [-inf, +inf] as the range of representable values.

Interesting element to remark (part of the code comments in that revision) is that operations resulting in NaN are not (yet) allowed:

// [expr.pre]p4:
//   If during the evaluation of an expression, the result is not
//   mathematically defined [...], the behavior is undefined.
// FIXME: C++ rules require us to not conform to IEEE 754 here.

Example at compiler explorer:

#include <limits>

constexpr double createNan()
{
    static_assert(std::numeric_limits<double>::is_iec559, "asdf");
    double d = std::numeric_limits<double>::infinity() / std::numeric_limits<double>::infinity();
    return -1*d;
}

static_assert(createNan() != 0., "NaN");

Upvotes: 2

Related Questions