4

The C++ standard 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.

and

The binary / operator yields the quotient from the division of the first expression by the second. If the quotient a/b is representable in the type of the result [snip]; otherwise, the behavior of a/b is undefined.

If I have:

double x = 1.0 / 3.0;

The result of the division of 1 by 3 is the real number "one third". One third is not representable in the type of the result (double, usually the IEEE754 double-precision binary64 floating point type).

Does that mean that the above statement is an undefined operation? Surely it cannot be - but I don't understand how it isn't?

Andrew Tomazos
  • 66,139
  • 40
  • 186
  • 319
  • Range here means `FLOATING_POINT_TYPE_MIN_VAL <= value <= FLOATING_POINT_TYPE_MAX_VAL`, not being exactly representable in the number of bits the type has. – NathanOliver Jun 17 '23 at 04:24
  • 3
    how do you represent it in decimal? 0.3? 0.333? 0.3333333? same problem with base 2 (or many other bases). – old_timer Jun 17 '23 at 04:33
  • `double` != real number. Some operation can be perfectly well define in terms of arithmetic operations on a C++ type, even though the result does not match the math on real numbers exactly. An even simpler example would be doing the same for `int`s (C++ expression `1/3`); the result is `0` which is clearly different to the real number "one third"; It's just that the C++ specification requires the expression to be treated differently to the operations on real numbers... – fabian Jun 17 '23 at 06:30
  • 1
    The mathematical value "one third" is between two values that a `double` can represent and the result of the expression `1.0/3.0` is one of those two values. Which value is picked is unspecified, not undefined (there is a finite number of possibilities, and an implementation is permitted to choose any of them) by the C++ standard. For IEEE floating point, the choice of those two values is determined by "rounding mode". Other floating point specifications may pick the closest, always round toward zero, or always round away from zero. – Peter Jun 17 '23 at 10:07
  • @old_timer: It could be reasonably stated that the real number one third is not representable in decimal. I think it's a bit of stretch to take the C++ standards use of the term "representable" as some sort of vague "approximately representable" meaning without a clarification or a definition. – Andrew Tomazos Jun 18 '23 at 09:08
  • it is not representable, has nothing to do with exceptions, they are just giving out additional advice about base 2 just like we already know things from grade school about base 10 (repetitive values or patterns to the right of the decimal point, same deal with floating point formats.) A comment on elementary math, has nothing to do with exceptions or faults or the floating point format used, etc. Most math fixed or float cannot be represented in the language or the formats so there is no way that that is a fault. One big company didnt even fault on divide by zero (fixed point). – old_timer Jun 18 '23 at 17:28
  • a fixed length mantissa is at the core of floating point – old_timer Jun 18 '23 at 18:32
  • @old_timer: It isn't a duplicate. See here for the correct answer: https://stackoverflow.com/a/76503833/1131467 – Andrew Tomazos Jun 19 '23 at 06:47

4 Answers4

6

The word "range" here refers to being in between the lowest and highest representable value. It's not the mathematical term as in "domain" and "range".

Real values can be out of range of a floating-point type due to being too large (far away from zero) or too small (nonzero values too close to zero).

Floating point types are inexact; that is in their nature. Some calculations produce a value which is an approximation to the exact real value. If such calculations produced garbage results or exceptions, floating-point would not meet its purpose.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • "or too small" that's never out of range. – n. m. could be an AI Jun 17 '23 at 04:24
  • @n.m.: `-1e500` is out of range of most floating point hardware because it's too small. – Jerry Coffin Jun 17 '23 at 04:26
  • 2
    All finite floating point values are _exact_. It is the operations that do not yield the exact mathematical result. – chux - Reinstate Monica Jun 17 '23 at 04:34
  • @JerryCoffin To me "small" always means "close to zero", never "far left from zero". – n. m. could be an AI Jun 17 '23 at 05:27
  • @chux-ReinstateMonica Whether floating-point values are exact or not depends on what they represent; we can't tell just by looking at them. Of course each one is nominally some rational number. But that rational number may represent a range of real numbers. Or it could just be itself. You can't tell by looking at it. A floating-point value could actually be a really bad approximation; e.g. 0.125 is exactly 1/8 in one very real sense (pardon the pun),but it could be a rounded off r measurement providing 3 significant figures, representing anything from 0.1245.. to 0.1254... – Kaz Jun 17 '23 at 05:30
  • @n.m.willseey'allonReddit We should adopt the word "deeper" for more negative. 1 is deeper than 4, -5 is deeper than -2. :) – Kaz Jun 17 '23 at 05:31
  • 1
    @Kaz: Re “Whether floating-point values are exact or not depends on what they represent; we can't tell just by looking at them.”: The IEEE-754 specification is clear: Each floating-point datum that is not a NaN or infinity represents one specific number exactly. It does not represent a range or interval. If a programmer writes code to treat a floating-point number as an interval, that is a feature of their program, not of floating-point numbers or floating-point arithmetic. One could do the same with an integer type, but we do not consider values of integer types to represent intervals. – Eric Postpischil Jun 17 '23 at 11:50
  • @EricPostpischil The IEEE-754 specification didn't invent floating-point numbers, so it doesn't get to dictate that to us. Say we have an A and B which are rationals exactly represented in floating-point. We divide C = A/B. In that situation if A/B is not representable,the floating point system chooses, as the result, some D which is very close to C. That D obviously represents the true result C. The idea of numbers representing other nearby numbers is baked into the floating-point concept, and has been before there was IEEE 754. – Kaz Jun 18 '23 at 14:59
  • @Kaz: Re “That D obviously represents the true result C”: No, that is not obvious, and it is false. With integers, the integer system chooses 2 as the result of 7/3, but that does not mean 2 represents 7/3. There is no basis for such a conclusion. Re “the floating point system chooses, as the result, some D which is very close to C”: Yes, it chooses some result to deliver. That does not mean the result represents A/B. The model used in IEEE-754 is the same in this regard as the model used in C, C++, textbooks, and academic papers. It is how proofs about floating-point arithmetic are done. – Eric Postpischil Jun 18 '23 at 15:31
  • @EricPostpischil Represents means something like "stands for'. For instance your `M_PI` constant represents π. When engineering and scientific formulas that contain π are translated to floating-point code, the `M_PI` constant or equivalent is used. It represents π. – Kaz Jun 18 '23 at 20:29
  • @Kaz: That is not the model used in IEEE 754, the C standard, the C++ standard, floating-point textbooks, or academic papers on floating-point. Working with floating-point arithmetic correctly sometimes requires analysis and proof. Trying to analyze floating-point software or write proofs using “represents” in such a crude way will not work. It is not engineering. We have too many problems with people not understanding floating-point code, so it is important to convey the model correctly. Floating-point behavior becomes clearer when the model is understood. – Eric Postpischil Jun 18 '23 at 22:39
2

I think you're mistaking "can't be represented" for "can't be represented perfectly".

At least with typical hardware, if I do something like:

double x = 1e300 / 1e-300;

The resulting value (1e600) is outside the range that can be represented (at all) so the result is undefined.

Something like 1.0 / 3.0 can't be represented exactly, but can be represented to some specified level of precision. And, of course, if somebody really wanted to, they could build base-3 floating point hardware that could represent 1.0/3.0 perfectly (but lose the ability to represent 1.0/2.0 perfectly, of course).

Looking at it from a slightly different direction, it largely comes down to the underlying hardware. Typically, you have IEEE 754 (or some variant of it). This standard defines the "correct" result (to the bit level) for something like 1.0/3.0 on binary hardware. Even though that is not exactly one third, it is what's defined as the "correct" result. As such, as long as the result you get is what IEEE 754 specifies, it's representing the correct result, even though that's different from what you'd expect in the realm of math on real numbers.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
1

Does that mean that the above statement is an undefined operation?

1.0/3.0 is OK.

When an math operator like / cannot return the exact mathematical result as a floating point value, the value returned is one of the two nearest representable floating point values, one larger, one smaller. That is rounding and the selection depends on rounding mode.

Of course with a domain problem, like x/0.0, even the math result is not defined.

Should there not exist two representable floating point values that bound the math result, that is when trouble occurs - leading to UB.

Often with infinity, there is always 2 bounding values.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
-1

It turns out the interpretation was incorrect and based on a misreading of the full text of the quoted sequence:

[expr.mul]p4:

For integral operands the / operator yields the algebraic quotient with any fractional part discarded; if the quotient a/b is representable in the type of the result, (a/b)*b + a%b is equal to a; otherwise, the behavior of both a/b and a%b is undefined.

https://eel.is/c++draft/expr.mul#4

The entire quoted sequence only applies to integral operands. In particular, the term "representable in the type" is only used in the context of integral operands and not floating-point operands.

Floating-point types have no "range of representable values" defined in the standard, nor is such a concept used in the standard.

Andrew Tomazos
  • 66,139
  • 40
  • 186
  • 319
  • What about https://eel.is/c++draft/expr#pre-4? – HolyBlackCat Jun 19 '23 at 06:51
  • @HolyBlackCat: What about it? That only applies to integral types., I think. The range of representable values of integral types is defined in [basic.fundamental]p1 and p2. There is no definition of range of representable values given for floating-point types. – Andrew Tomazos Jun 19 '23 at 07:04
  • It doesn't explicitly limit itself to integral types. *"There is no definition of range of representable values given for floating-point types"* Sounds like an editoral error to me, I doubt the intent was to implicitly limit this to integers based on such a minute detail. The explanation by @Kaz makes more sense to me. (but I didn't downvote) – HolyBlackCat Jun 19 '23 at 07:12
  • @HolyBlackCat: I agree, but the "range of representable values" for floating-point types is not defined, so I'm not sure what else to make of [expr.pre]p4 other than adding an implicit "if the type has a representable range of values then". If the range of representable values of floating-points were defined, I would expect them to be defined in [basic.fundamental]p12. – Andrew Tomazos Jun 19 '23 at 07:20
  • "but the "range of representable values" for floating-point types is not defined" It is defined by the semantics of ``, specifically it is precisely `[numeric_limits::lowest() .. numeric_limits::max()]` – n. m. could be an AI Jun 19 '23 at 10:15