NaN ASCII I/O с Visual С++

Я хочу читать и записывать значения NaN из/в текстовые файлы, используя iostream и Visual С++. При написании значения NaN я получаю 1.#QNAN. Но, читая его, выходы 1.0.

float nan = std::numeric_limits<float>::quiet_NaN ();
std::ofstream os("output.txt");

os << nan ;
os.close();

Выходной сигнал 1.#QNAN.

std::ifstream is("output.txt");
is >> nan ;
is.close();

nan равно 1.0.

Решение

Наконец, как было предложено awoodland, я придумал это решение. Я выбрал "nan" как строковое представление NaN. Оба < и → операторы переопределены.

using namespace ::std;

class NaNStream 
{
public:
  NaNStream(ostream& _out, istream& _in):out(_out), in(_in){}
  template<typename T>
  const NaNStream& operator<<(const T& v) const {out << v;return *this;}
  template<typename T>
  const NaNStream& operator>>(T& v) const {in >> v;return *this;}
protected:
  ostream& out;
  istream& in;
};

// override << operator for float type
template <> const NaNStream& NaNStream::operator<<(const float& v) const 
{
  // test whether v is NaN 
  if( v == v )
    out << v;
  else
    out << "nan";
  return *this;
}

// override >> operator for float type
template <> const NaNStream& NaNStream::operator>>(float& v) const 
{
  if (in >> v)
    return *this;

  in.clear();
  std::string str;
  if (!(in >> str))
    return *this;

  if (str == "nan")
    v = std::numeric_limits<float>::quiet_NaN();
  else
    in.setstate(std::ios::badbit); // Whoops, we've still "stolen" the string

  return *this;
}

Минимальный рабочий пример: конечный float и NaN записываются в строковый поток и затем читаются.

int main(int,char**) 
{
  std::stringstream ss;
  NaNStream nis(ss, ss);
  nis << 1.5f << std::numeric_limits<float>::quiet_NaN ();
  std::cout << ss.str() << std::endl; // OUTPUT : "1.5nan"

  float a, b;
  nis >> a;  nis >> b;
  std::cout << a << b << std::endl;  // OUTPUT : "1.51.#QNAN"
}

Ответы

Ответ 1

С С++ 03 вы можете довольно легко обойти проблему с помощью вспомогательного класса и вашего собственного оператора:

#include <iostream>
#include <sstream>
#include <string>
#include <limits>

struct FloatNaNHelper {
  float value;
  operator const float&() const { return value; }
};

std::istream& operator>>(std::istream& in, FloatNaNHelper& f) {
  if (in >> f.value)
    return in;

  in.clear();
  std::string str;
  if (!(in >> str))
    return in;

  // use std::transform for lowercaseness?
  // NaN on my platform is written like this.
  if (str == "NaN")
    f.value = std::numeric_limits<float>::quiet_NaN();
  else
    in.setstate(std::ios::badbit); // Whoops, we've still "stolen" the string

  return in;
}

Это работает для NaN на моей платформе довольно разумно, но также иллюстрирует проблему переносимости, присущую ей, - ваша библиотека, по-видимому, представляет ее по-разному, что может немного осложнить проблему, если вы хотите поддержать ее. Я использовал этот тест:

int main() {
  std::istringstream in("1.0 555 NaN foo");
  FloatNaNHelper f1,f2,f3;
  in >> f1 >> f2 >> f3;
  std::cout << static_cast<float>(f1) << ", " << static_cast<float>(f2) << ", " << static_cast<float>(f3) << std::endl;

  if (in >> f1)
    std::cout << "OOPS!" << std::endl;
}

Вы также можете изменить семантику этого на что-то, возможно, немного чище:

int main() {
  std::istringstream in("1.0 555 NaN foo");
  float f1,f2,f3;
  in >> FloatNaNHelper(f1) >> FloatNaNHelper(f2) >> FloatNaNHelper(f3);
  std::cout << f1 << ", " << f2 << ", " << f3 << std::endl;
}

Требуется изменить FloatNaNNHelper:

struct FloatNaNHelper {
  float& value;
  explicit FloatNaNHelper(float& f) : value(f) { }
};

И оператор:

std::istream& operator>>(std::istream& in, const FloatNaNHelper& f);

Ответ 2

При печати значения float или double в std::ostream используется шаблон класса std::num_put<> (С++ 03 §22.2.2.2). Он форматирует значение, как если бы оно было напечатано printf с одним из спецификаторов формата %e, %E, %f, %g или %g, в зависимости от флагов потока (Таблица 58).

Аналогично, при вводе значения float или double он считывает его, как если бы с помощью функции scanf с спецификатором формата %g (§22.2.2.1.2/5).

Итак, следующий вопрос: почему scanf неправильно анализирует 1.#QNAN. В стандарте C89 не упоминаются NaN в описаниях функций fprintf и fscanf. Он говорит, что представление чисел с плавающей запятой неуказано, поэтому это приводит к неуказанному поведению.

C99, с другой стороны, определяет здесь поведение. Для fprintf (C99 §7.19.6.1/8):

A double аргумент, представляющий бесконечность, преобразуется в один из стилей [-]inf или [-]infinity - этот стиль определяется реализацией. Аргумент double, представляющий NaN, преобразуется в один из стилей [-]nan или [-]nan(n-char-sequence) - какой стиль и значение любая n- char -последовательность, определяется реализацией. Спецификатор преобразования F производит INF, INFINITY или NAN вместо INF, INFINITY или NAN, соответственно. 243)

fscanf указан для анализа числа в соответствии с strtod(3) (C99 §7.19.6.2/12). strtod анализируется следующим образом (§7.20.1.3/3):

Ожидаемая форма субъектной последовательности - необязательный знак плюс или минус, затем один из следующее:
- непустая последовательность десятичных цифр, необязательно содержащая десятичную точку символ, затем необязательную часть экспоненты, как определено в 6.4.4.2;
- a 0x или 0x, то непустая последовательность шестнадцатеричных цифр, необязательно содержащих знак десятичной точки, затем необязательную двоичную часть экспоненты, как определено в 6.4.4.2;
- INF или INFINITY, игнорируя случай - NAN или NAN(n-char-sequenceopt), игнорируя случай в части NAN, где:

n-char-sequence:
    digit
    nondigit
    n-char-sequence digit
    n-char-sequence nondigit
Последовательность субъекта определяется как самая длинная начальная подпоследовательность входной строки, начиная с первого символа небелого пробела, который имеет ожидаемую форму. Тема последовательность не содержит символов, если строка ввода не соответствует ожидаемой форме.

Итак, после всего этого, конечный результат заключается в том, что ваша стандартная библиотека C не совместима с C99, так как 1.#QNAN не является допустимым выходом fprintf в соответствии с вышеизложенным. Но хорошо известно, что среда исполнения Microsoft C не совместима с C99, и, насколько мне известно, она не собирается становиться совместимой в ближайшее время. Поскольку C89 не определяет поведение здесь в отношении NaN, вам не повезло.

Вы можете попробовать переключиться на другой компилятор и C runtime (например, Cygwin + GCC), но это ужасно большой молот для такого маленького гвоздя. Если вам действительно нужно это поведение, я бы рекомендовал написать класс-оболочку для float, который способен правильно форматировать и анализировать значения NaN.