Новый манипулятор вывода чисел с плавающей точкой
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 без значимой потери точности
-
Суть та же, приходится выбирать один из вариантов поведения. Опция
#
("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) тоже добавляет бесполезные нули. -
Например при парсинге 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.0
на1.1
или наоборот. -
Насколько я понимаю
std::format
не является заменой манипулятору -- непосредственный вывод может производиться внешней библиотекой. -
Возможно могут быть подводные камни в не-C локалях.
Слепил простой демонстратор моих хотелок если кому-то интересно -> https://github.com/asherikov/numdata.