Как я могу упростить работу с сигналами NaNs?
Стандарт IEEE754 определяет два класса NaN, тихое NaN, QNaN и сигнализацию NaN, SNaN. Когда SNaN загружается в регистр с плавающей запятой, исключение генерируется блоком с плавающей точкой.
QNaN доступен для кода Delphi через константу с именем NaN
, которая объявлена в Math
. Определение этой константы:
const
NaN = 0.0 / 0.0;
Я хотел бы иметь возможность использовать что-то похожее, чтобы объявить константу, которая является сигнальной NaN, но еще не нашла способ сделать это.
Наивно вы можете написать этот код:
function SNaN: Double;
begin
PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;//this bit pattern specifies an SNaN
end;
Но ABI для значений возвращаемых значений с плавающей запятой означает, что SNaN загружается в регистр с плавающей запятой, чтобы он мог быть возвращен. Естественно, это приводит к исключению, которое скорее поражает цель.
Итак, вы затем приводите к написанию кода следующим образом:
procedure SetToSNaN(out D: Double);
begin
PInt64(@D)^ := $7FF7FFFFFFFFFFFF;
end;
Теперь это работает, но это очень неудобно. Предположим, вам нужно передать SNaN в другую функцию. В идеале вы хотели бы написать:
Foo(SNaN)
но вместо этого вы должны сделать это:
var
SNaN: Double;
....
SetToSNaN(SNaN);
Foo(SNaN);
Итак, после наращивания, вот вопрос.
Есть ли способ написать x := SNaN
и иметь переменную с плавающей запятой x
присвоенное значение, которое является сигналом NaN?
Ответы
Ответ 1
Это объявление разрешает его во время компиляции:
const
iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
SNaN : Double absolute iNaN;
Компилятор по-прежнему обрабатывает SNaN
как константу.
Попытка присвоить значение SNaN
даст ошибку времени компиляции: 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"
Если у вас есть директива компилятора $WRITEABLECONSTS ON
(или $J+
), это можно временно отключить, чтобы не изменять SNaN
.
{$IFOPT J+}
{$DEFINE UNDEFWRITEABLECONSTANTS}
{$J-}
{$ENDIF}
const
iNaN : UInt64 = $7FF7FFFFFFFFFFFF;
var
SNaN : Double ABSOLUTE iNaN;
{$IFDEF UNDEFWRITEABLECONSTANTS}
{$J+}
{$ENDIF}
Ответ 2
Вы можете встроить функцию:
function SNaN: Double; inline;
begin
PInt64(@Result)^ := $7FF7FFFFFFFFFFFF;
end;
Но это будет зависеть от настроения оптимизации и компилятора.
Я видел некоторые функции, не встроенные, без ясного понимания из контекста. Я не люблю либо полагаться на inlining.
Что мне лучше делать, и что будет работать во всех версиях Delphi, - это использовать глобальную переменную:
var
SNaN: double;
Затем установите его в блок initialization
устройства:
const
SNaN64 = $7FF7FFFFFFFFFFFF;
initialization
PInt64(@SNaN)^ := SNaN64;
end.
Тогда вы сможете использовать SNaN
как постоянную константу. То есть вы можете написать код, как ожидалось:
var test: double;
...
test := SNaN;
В отладчике IDE он будет показан как "test = + NAN", который является ожидаемым результатом. Я полагаю.
Обратите внимание, что использование этого SNaN
вызовет исключение, когда оно будет прочитано в стек FPU (например, if test=0 then
), поэтому вам нужно проверить значение на двоичном уровне... вот почему я определил a SNaN64
constant, что сделает очень быстрый код, кстати.
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);
Вы можете изменить это поведение, изменив регистр исключений x87:
backup := Set8087CW($133F);
try
..
finally
Set8087CW(backup);
end;
Я предполагаю, что это будет установлено глобально для вашей программы, во всех аспектах кода, который должен будет обрабатывать эту константу SNaN
.
Ответ 3
Вот еще одно обходное решение:
type
TFakeRecord = record
case Byte of
0: (SNaN: Double);
1: (i: Int64);
end;
const
IEEE754: TFakeRecord = ( i: $7FF7FFFFFFFFFFFF);
Отладчик показывает IEEE754.SNaN как + NAN, однако при его доступе вы все равно получите исключение с плавающей запятой. Обходной путь для этого может быть:
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;
Ответ 4
Я использую функцию:
Function UndefinedFloat : double
Begin
Result := Nan
End;
This then works
Var
MyFloat : double;
Begin
MyFloat := UndefinedFloat;
Ответ 5
Здесь довольно грязный способ сделать это, что приводит к очень чистому коду для потребителя.
unit uSNaN;
interface
const
SNaN: Double=0.0;//SNaN value assigned during initialization
implementation
initialization
PInt64(@SNaN)^ := $7FF7FFFFFFFFFFFF;
end.
Я ожидал, что компоновщик поместит SNaN
в сегмент только для чтения исполняемого файла, но, похоже, он этого не сделает. В любом случае, даже если бы это было возможно, вы могли бы использовать VirtualProtect
, чтобы обойти это на время назначения.