问题
The IEEE754 standard defines two classes of NaN, the quiet NaN, QNaN, and the signaling NaN, SNaN. When an SNaN is loaded into a floating point register, an exception is raised by the floating point unit.
QNaN is available to Delphi code through the constant named NaN that is declared in Math. The definition of that constant is:
const
NaN = 0.0 / 0.0;
I would like to be able to use something similar to declare a constant that is a signaling NaN, but have not yet found a way to do that.
Naively you might write this code:
function SNaN: Double;
begin
PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;//this bit pattern specifies an SNaN
end;
But the ABI for floating point return values means that the SNaN is loaded into a floating point register so that it can be returned. Naturally that leads to an exception which rather defeats the purpose.
So you are then led to writing code like this:
procedure SetToSNaN(out D: Double);
begin
PInt64(@D)^ := $7FF7FFFFFFFFFFFF;
end;
Now, this works, but it's very inconvenient. Suppose you need to pass an SNaN to another function. Ideally you would like to write:
Foo(SNaN)
but instead you have to do this:
var
SNaN: Double;
....
SetToSNaN(SNaN);
Foo(SNaN);
So, after the build-up, here's the question.
Is there any way to write x := SNaN and have the floating point variable x assigned a value that is a signaling NaN?
回答1:
This declaration solves it at compile time:
const
iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
SNaN : Double absolute iNaN;
The compiler still treats the SNaN as a constant.
Trying to assign a value to SNaN will give a compile time error: E2064 Left side cannot be assigned to.
procedure DoSomething( var d : Double);
begin
d := 2.0;
end;
SNaN := 2.0; // <-- E2064 Left side cannot be assigned to
DoSomething( SNaN); // <--E2197 Constant object cannot be passed as var parameter
WriteLn(Math.IsNaN(SNaN)); // <-- Writes "true"
Should you have the compiler directive $WRITEABLECONSTS ON (or $J+), this could be turned off temporarily to ensure not altering SNaN.
{$IFOPT J+}
{$DEFINE UNDEFWRITEABLECONSTANTS}
{$J-}
{$ENDIF}
const
iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
SNaN : Double ABSOLUTE iNaN;
{$IFDEF UNDEFWRITEABLECONSTANTS}
{$J+}
{$ENDIF}
回答2:
Here's another workaround:
type
TFakeRecord = record
case Byte of
0: (SNaN: Double);
1: (i: Int64);
end;
const
IEEE754: TFakeRecord = ( i: $7FF7FFFFFFFFFFFF);
The debugger shows IEEE754.SNaN as +NAN, however when you access it you'll still get a floating point exception. A workaround for that could be:
type
ISet8087CW = interface
end;
TISet8087CW = class(TInterfacedObject, ISet8087CW)
protected
OldCW: Word;
public
constructor Create(const NewCW: Word);
destructor Destroy; override;
end;
TIEEE754 = record
case Byte of
0: (SNaN: Double);
1: (i: Int64);
end;
const
IEEE754: TIEEE754 = ( i: $7FF7FFFFFFFFFFFF);
{ TISet8087CW }
constructor TISet8087CW.Create(const NewCW: Word);
begin
OldCW := Get8087CW;
Set8087CW(NewCW);
inherited Create;
end;
destructor TISet8087CW.Destroy;
begin
Set8087CW(OldCW);
inherited;
end;
procedure TForm6.Button4Click(Sender: TObject);
var
CW: ISet8087CW;
begin
CW := TISet8087CW.Create($133F);
Memo1.Lines.Add(Format('SNaN: %f', [IEEE754.SNaN]));
end;
回答3:
You can inline the function:
function SNaN: Double; inline;
begin
PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;
end;
But it will depend on the optimization and compiler mood.
I've seen some functions not inlined, without any clear understanding from the context. I do not like either relying on inlining.
What I would better do, and which will work on all versions of Delphi, is to use a global variable:
var
SNaN: double;
Then set it in the initialization block of the unit:
const
SNaN64 = $7FF7FFFFFFFFFFFF;
initialization
PInt64(@SNaN)^ := SNaN64;
end.
Then you will be able to use SNaN just as a regular constant. That is, you can write code as expected:
var test: double;
...
test := SNaN;
In the IDE debugger, it will be shown as "test = +NAN", which is the expected result, I suppose.
Note that using this SNaN will raise an exception when it is read into the FPU stack (e.g. if test=0 then) so you have to check the value at binary level... this is the reason why I defined a SNaN64 constant, which will make very fast code by the way.
toto := SNaN;
if PInt64(@toto)^<>SNaN64 then // will run and work as expected
DoubleToString(toto);
if toto<>SNaN then // will raise an EInvalidOp at runtime
DoubleToString(toto);
You can change this behavior by changing the x87 exception register:
backup := Set8087CW($133F);
try
..
finally
Set8087CW(backup);
end;
I suppose this to be set globally for your program, in all extend of the code which will have to handle this SNaN constant.
回答4:
I use a function:
Function UndefinedFloat : double
Begin
Result := Nan
End;
This then works
Var
MyFloat : double;
Begin
MyFloat := UndefinedFloat;
回答5:
Here's a rather dirty way to do it, that results in very clean code for the consumer.
unit uSNaN;
interface
const
SNaN: Double=0.0;//SNaN value assigned during initialization
implementation
initialization
PInt64(@SNaN)^ := $7FF7FFFFFFFFFFFF;
end.
I was expecting the linker to put SNaN in a read-only segment of the executable but it appears not to do so. In any case, even if it did you could use VirtualProtect to get around that for the duration of the assignment.
来源:https://stackoverflow.com/questions/16249748/how-can-i-make-signaling-nans-easy-to-work-with