Почему Clang std :: ostream пишет двойной, который std :: istream не может читать?

Я использую приложение, которое использует std::stringstream для чтения матрицы пространства, разделенной double от текстового файла. Приложение использует код, немного похожий:

std::ifstream file {"data.dat"};
const auto header = read_header(file);
const auto num_columns = header.size();
std::string line;
while (std::getline(file, line)) {
    std::istringstream ss {line}; 
    double val;
    std::size_t tokens {0};
    while (ss >> val) {
        // do stuff
        ++tokens;
    }
    if (tokens < num_columns) throw std::runtime_error {"Bad data matrix..."};
}

Довольно стандартный материал. Я старательно написал код для создания матрицы данных (data.dat), используя следующий метод для каждой строки данных:

void write_line(const std::vector<double>& data, std::ostream& out)
{
    std::copy(std::cbegin(data), std::prev(std::cend(data)),
              std::ostream_iterator<T> {out, " "});
    out << data.back() << '\n';
}

т.е. используя std::ostream. Однако я обнаружил, что приложение не прочитало мой файл данных с помощью этого метода (выбрасывая исключение выше), в частности, он не читал 7.0552574226130007e-321.

Я написал следующий минимальный тестовый пример, который показывает поведение:

// iostream_test.cpp

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

int main()
{
    constexpr double x {1e-320};
    std::ostringstream oss {};
    oss << x;
    const auto str_x = oss.str();
    std::istringstream iss {str_x};
    double y;
    if (iss >> y) {
        std::cout << y << std::endl;
    } else {
        std::cout << "Nope" << std::endl;
    }
}

Я тестировал этот код на LLVM 10.0.0 (clang-1000.11.45.2):

$ clang++ --version
Apple LLVM version 10.0.0 (clang-1000.11.45.2)
Target: x86_64-apple-darwin17.7.0 
$ clang++ -std=c++14 -o iostream_test iostream_test.cpp
$ ./iostream_test
Nope

Я также попробовал компиляцию с Clang 6.0.1, 6.0.0, 5.0.1, 5.0.0, 4.0.1 и 4.0.0, но получил тот же результат.

Компиляция с GCC 8.2.0, код работает так, как я ожидал:

$ g++-8 -std=c++14 -o iostream_test iostream_test.cpp
$ ./iostream_test.cpp
9.99989e-321

Почему существует разница между Clang и GCC? Является ли это ошибкой clang, а если нет, как следует использовать потоки C++ для записи переносимого ввода-вывода с плавающей запятой?

Ответ 1

Я считаю, что clang здесь соответствует, если мы читаем ответ на std :: stod throws out_of_range error для строки, которая должна быть действительной, она говорит:

Стандарт C++ позволяет конверсиям строк double отчет о нижнем уровне, если результат находится в субнормальном диапазоне, даже если он представлен.

7.63918 • 10 -313 находится в пределах double, но находится в субнормальном диапазоне. Стандарт C++ говорит, что stod вызывает strtod а затем отсылает к стандарту C для определения strtod. Стандарт C указывает на то, что strtod может переполняться, о чем говорится: "Результат заканчивается, если величина математического результата настолько мала, что математический результат не может быть представлен без особой ошибки округления в объекте указанного типа". Это неловкое выражение, но оно относится к ошибкам округления, которые возникают при возникновении субнормальных значений. (Субнормальные значения подвержены более крупным относительным ошибкам, чем нормальные значения, поэтому их ошибки округления могут считаться экстраординарными).

Таким образом, реализация C++ разрешена стандартом C++ до нижнего уровня для субнормальных значений, даже если они являются представимыми.

Мы можем подтвердить, что мы полагаемся на strtod из [facet.num.get.virtuals] p3.3.4:

  • Для двойного значения используется функция strtod.

Мы можем проверить это с помощью этой небольшой программы (см. Ее в прямом эфире):

void check(const char* p) 
{
  std::string str{p};

    printf( "errno before: %d\n", errno ) ;
    double val = std::strtod(str.c_str(), nullptr);
    printf( "val: %g\n", val ) ;
    printf( "errno after: %d\n", errno ) ;
    printf( "ERANGE value: %d\n", ERANGE ) ;

}

int main()
{
 check("9.99989e-321") ;
}

который имеет следующий результат:

errno before: 0
val: 9.99989e-321
errno after: 34
ERANGE value: 34

C11 в 7.22.1.3p10 говорит нам:

Функции возвращают преобразованное значение, если оно есть. Если преобразование не может быть выполнено, возвращается ноль. Если правильное значение переполнения и округление по умолчанию действует (7.12.1), возвращается плюс или минус HUGE_VAL, HUGE_VALF или HUGE_VALL (в соответствии с типом возврата и значком значения), и сохраняется значение макроса ERANGE в errno. Если результат заканчивается (7.12.1), функции возвращают значение, величина которого не больше наименьшего нормализованного положительного числа в возвращаемом типе; независимо от того, получает ли errno значение ERANGE, оно определяется реализацией.

POSIX использует это соглашение:

[ERANGE]
Возвращаемое значение приведет к переполнению или недопущению.

Мы можем проверить, является ли он субнормальным через fpclassify (см. Его в прямом эфире).