cpp-ru/ideas

Новый манипулятор вывода чисел с плавающей точкой

asherikov opened this issue · 4 comments

Добавить манипулятор вывода чисел с плавающей точкой для компактного форматирования без потери точности и типа, существующие механизмы этого не позволяют (https://en.cppreference.com/w/cpp/io/manip):

  • std::setprecision низкоуровневая функция, в сочетании со стандартным форматированием целочисленных значений теряется точка при выводе, что приводит к потере информации о типе и возможным проблемам в обрабатывающем коде на других языках;
  • вывод std::scientific или std::setprecision + std::showpoint избыточен.

Пример:

#include <iostream>
#include <iomanip>
#include <limits>

int main()
{
    double a = 1.;
    double b = 0.0000000000001;

    std::cout << a << " | "  << b << std::endl;
    std::cout << std::scientific << a << " | "  << b << std::endl;
    std::cout << std::defaultfloat;
    std::cout << std::setprecision(std::numeric_limits<long double>::digits10 + 1) << a << " | " << b << std::endl;
    std::cout << std::defaultfloat;
    std::cout << std::showpoint << std::setprecision(std::numeric_limits<long double>::digits10 + 1) << a << " | " << b << std::endl;

    return 0;
}

Результат:

1 | 1e-13
1.000000e+00 | 1.000000e-13
1 | 1.00000000000000003e-13
1.000000000000000000 | 1.000000000000000030e-13

Хочется:

// std::cout << std::numericdata << a << " | " << b << std::endl;
1. | 1.00000000000000003e-13

Сейчас комитет отходит от iostream в пользу форматирования через std::format или std::print. У них есть форматтеры :f, которые всегда выводят без экспоненты и с точкой:

#include <iostream>
#include <print>

int main()
{
    double a = 1.;
    double b = 0.0000000000001;
    double с = 0.3000020000001;
    auto digits = std::numeric_limits<double>::digits10 + 1;
    constexpr auto format_str = R"(
      {2} =>
        :g          {0:g}
        :e          {0:e}
        :f          {0:f}
        :.digits_g  {0:.{1}g}
        :.digits_e  {0:.{1}e}
        :.1_e       {0:.1e}
        :.digits_f  {0:.{1}f}
        :<1_e       {0:<1e}
        :e<1        {0:e<0}
    )";
    std::print(std::cout, format_str, a, digits, "1.");
    std::print(std::cout, format_str, b, digits, "0.0000000000001");
    std::print(std::cout, format_str, с, digits, "0.3000020000001");
}

Вывод:

      1. =>
        :g          1
        :e          1.000000e+00
        :f          1.000000
        :.digits_g  1
        :.digits_e  1.0000000000000000e+00
        :.1_e       1.0e+00
        :.digits_f  1.0000000000000000
        :<1_e       1.000000e+00
        :e<1        1
    
      0.0000000000001 =>
        :g          1e-13
        :e          1.000000e-13
        :f          0.000000
        :.digits_g  1e-13
        :.digits_e  1.0000000000000000e-13
        :.1_e       1.0e-13
        :.digits_f  0.0000000000001000
        :<1_e       1.000000e-13
        :e<1        1e-13
    
      0.3000020000001 =>
        :g          0.300002
        :e          3.000020e-01
        :f          0.300002
        :.digits_g  0.3000020000001
        :.digits_e  3.0000200000010002e-01
        :.1_e       3.0e-01
        :.digits_f  0.3000020000001000
        :<1_e       3.000020e-01
        :e<1        0.3000020000001

Онлайн песочница с примером https://godbolt.org/z/PG8Pd4nfP

Однако эти форматы либо не ставят .0 перед экспонентой, либо не теряют в точности, либо не предоставляют наиболее короткое предстваление. кажется что поведение по умолчани, (:g формат) ближе всего, хоть и не ставят .0 перед экспонентой

А зачем понадобилось иметь именно .0 перед экспонентой? Запись числа - плохой источник информации о типе, т.к. 1.0e3 может быть представимо в виде int, short, double, float, rational, decimal без значимой потери точности

  1. Суть та же, приходится выбирать один из вариантов поведения. Опция # ("For floating-point types, the alternate form causes the result of the conversion of finite values to always contain a decimal-point character", https://en.cppreference.com/w/cpp/utility/format/spec) тоже добавляет бесполезные нули.

  2. Например при парсинге YAML файлов сгенерированых программой на C++ из питона значения интерпретируются как целые или как числа с плавающей точкой в зависимости от наличия точки:

# 1.yaml
a:
    b: 1
    c: 1.
import yaml
with open('1.yaml', 'r') as file:
    x = yaml.safe_load(file)
type(x['a']['b']) # int
type(x['a']['c']) # float

Понятно что это ненадежный источник информации о типе, но она может служить подсказкой парсеру. Здесь вопрос скорее не в надежности представления типа, а в том что более соотвествует ожиданиям и позволит избежать некоторых ошибок. boost::lexical_cast, например, не распарсит 1. как int.

Пара замечаний в догонку:

  1. Имеет значение не только то что тип может быть угадан неправильно, но и то что он может быть угадан по разному, например, если значение сменилось с 1.0 на 1.1 или наоборот.

  2. Насколько я понимаю std::format не является заменой манипулятору -- непосредственный вывод может производиться внешней библиотекой.

  3. Возможно могут быть подводные камни в не-C локалях.

Слепил простой демонстратор моих хотелок если кому-то интересно -> https://github.com/asherikov/numdata.