Как я могу упростить работу с сигналами 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, чтобы обойти это на время назначения.