Repository navigation
Fix the text output of to_chars, snprintf, operator<< and std::format - #1536
Merged
Merged
Conversation
|
An automated preview of the documentation is available at https://1536.decimal.prtest3.cppalliance.org/libs/decimal/doc/html/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-10-27 04:20:20 UTC |
ibmibmibm
force-pushed
the
ostream-fixes
branch
2 times, most recently
from
October 26, 2026 08:07
cc3f0d2 to
f9404c0
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #1536 +/- ##
=========================================
- Coverage 98.5% 98.4% -0.0%
=========================================
Files 326 331 +5
Lines 27695 28245 +550
Branches 2305 2326 +21
=========================================
+ Hits 27255 27784 +529
- Misses 440 461 +21
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
ibmibmibm
marked this pull request as ready for review
October 27, 2026 04:13
- to_chars_integer_impl divided a 128-bit integer by 10^9 for each group of 9 digits. This is up to 5 slow 128-bit divisions. - It now divides once by 10^19 with divmod_pow10_uint128. The 64-bit remainder gives the last 19 digits. A quotient below 10^19 takes the 64-bit path, and a larger quotient is one digit and 19 more digits. - The new to_chars_19_digits writes 19 digits with the leading zeros. - The results do not change. - Remove the includes that to_chars_integer_impl.hpp does not use: <array>, <utility>, <cstring>, <cstdio>, <cerrno>, <cmath> and a second <limits>.
- to_chars_round divides by the dropped power of ten once, keeps the remainder as the sticky bit, and rounds with the sign of the value. - The scientific format moves the exponent when the result gets one more digit. - The scientific path compares the precision without an overflow, checks the room for the minus sign, and checks the room for the trailing zeros without a pointer past the buffer. - The fixed format with a precision has a new path, to_chars_fixed_precision. It rounds |x| * 10^precision to an integer and puts the point in front of its last precision digits. - to_chars_fixed_precision computes its lengths in 64 bits, so a precision near INT_MAX gives value_too_large and not a signed overflow. - to_chars_fixed_impl now handles only the general format, so its branches for the fixed format are removed. - charconv.hpp includes <limits>, <system_error> and <type_traits>, which it uses. Fixes boostorg#1530
- The new to_chars_general_precision rounds the significand once to P digits, where P is the precision and 0 counts as 1. It uses the fixed format when the exponent X after the rounding is in [-4, P), and the scientific format for other values. It removes the trailing zeros. - to_chars_fixed_impl now gets only the fixed format, so its general format code is removed. - The fixed path and the nonfinite path check their own room, so the shared precision check is removed. - The zero and the negative nan check the room for their exact length, without an overflow. - The scientific format checks the room for the exponent before it writes. Its code for the general format is removed, because the general format reaches it only for zero, inf and nan. - The decimal32 stream tests now expect the std::ostream output for a double. Fixes boostorg#1531
- The parser reads the flags "-", "+", " " and "0", the field width, the precision, the type modifier and the conversion character, and stops after it. - Each value is cast to the type of its modifier first. The cast value sizes the buffer, decides the 0 flag and gets printed, so a value that narrows to inf pads with spaces. - Each value is formatted into a buffer of the to_chars bound plus the length of the integer part for the separators. The buffer is on the heap when the value is long. The value is then copied with its padding into the output. - The new detail::to_chars_length gives the to_chars bound from the format, the precision and the exponent. It keeps the general format to the digits of the type, and the fixed format to its integer digits and precision. inf and nan do not use the precision, so a large precision does not size their buffer. - The 0 flag pads every finite value with zeros, also a hex value that starts with a letter. - A long padding goes into the output in one block, and the part that does not fit is only counted. - When the output does not fit, snprintf writes what fits, adds the null terminator and returns the length of the full output, as std::snprintf does. - A conversion with no value returns -1 with errno EINVAL. - A width or a precision above INT_MAX, or a full count above INT_MAX, returns -1 with EOVERFLOW, as printf does. - fprintf formats once into a local buffer, or into a heap buffer of the length that the first pass returned. If the second pass fails, fprintf writes nothing and returns -1. - The grouping uses all the group sizes, and stops at the first character that is not a digit. detail::group_size and detail::count_separators hold the grouping rule for snprintf and operator<<. - A hex value gets the decimal point of the locale but no grouping, as printf does. - The separators go in by offsets from the start of the buffer, so no pointer goes before it. - make_uppercase moves to charconv.hpp, so that operator<< and the formatters use the same ASCII rule. - cstdio.hpp includes <initializer_list> and <type_traits>, which it uses. locale_conversion.hpp includes <string> for the grouping, and no longer includes <clocale>, which it does not use. Fixes boostorg#1532
- Take the format from the float field, as for a double. - Use the stream precision, with a negative precision as 6. - Format into a local buffer. When the value does not fit, format on the heap with the length that the precision and the exponent give. A failed conversion sets badbit. - Size the buffer for showpoint with the general format as for the scientific format, because showpoint keeps the trailing zeros. - Apply uppercase, showpos and showpoint. The general format keeps the trailing zeros as printf %#g, and hexfloat keeps the point as %#a. - Write the value with the fill as num_put does, with the runs of characters widened and written in blocks. - Reset the width before the output, so that it also resets when the stream buffer throws. - Take the grouping and the decimal point from the numpunct of the stream character type. Put the separators only between the digits before the point, with detail::count_separators. - If an exception occurs during the output, set badbit. Send the exception to the caller only when badbit is in the exception mask. - Remove the -Wformat-truncation pragma, which guards no snprintf call. - io.hpp includes <locale> and <new>, which it uses, and no longer includes <cinttypes>, which it does not use. Fixes boostorg#1533
- format.hpp includes <version> first, as the vendored int128 format.hpp does. - The two formatters share detail::parse_format_spec and detail::format_decimal in the new detail/format_render.hpp, with the sign and alignment enums and a detail::format_spec. - The buffer has the length that detail::to_chars_length gives. It is on the stack for a short value. - A failed conversion throws format_error, or writes nothing without exceptions. - With no alignment, the 0 flag pads with zeros after the sign, and with spaces for infinity and NaN. - The locale grouping comes before the padding. - L is accepted before and after the type. - L writes the value as operator<< does, with the locale of the format context. Each formatter gives only the locale access of its context. - The fill keeps the output character type. - The uppercase changes only the ASCII letters. - The parser rejects a and A of a fast type with format_error. - A width or a precision above INT_MAX gives format_error. - A bad specification throws the format_error of the library. Without exceptions, the parse stops at the bad character, so that the library rejects it. - The padding goes straight to the output, and only L builds a string. - Update format.adoc. - format.hpp and fmt_format.hpp include <type_traits>, and fmt_format.hpp includes <locale>, which they use. They no longer include <iostream>, <iomanip> and <format>, which they do not use. Fixes boostorg#1534
- Compare the separator as unsigned char with 0xE2, the first byte of the UTF-8 U+202F, in one detail::thousands_separator. - Count the grouping in std::size_t, so the cast to int is gone. - Copy into the bytes array of the u256. - Remove the pragmas. - u256.hpp includes <cstring>, <limits> and <type_traits>, and <ostream> for its operator<<. It no longer includes <cmath>, which it does not use. Fixes boostorg#1535
ibmibmibm
force-pushed
the
ostream-fixes
branch
from
October 27, 2026 04:14
f9404c0 to
baaf14f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR has 7 commits. Each commit after the first one fixes one issue, and each later fix uses the earlier ones.
to_chars_integer_implprints a 128-bit integer with one division by 10^19 and a 64-bit remainder, in place of up to 5 128-bit divisions by 10^9. The output does not change.to_charswith a precision rounds the significand once with all the dropped digits, and the sign of the value. The fixed format with a precision has a new path that rounds |x| * 10^precision to an integer. The scientific format checks the room for the sign, the trailing zeros and the exponent before it writes (to_chars with a precision rounds some values in the wrong direction #1530).to_charswith the general format and a precision gives the output of printf%g. Each path checks the room for its exact length. A short buffer givesvalue_too_large, and nothing is written past its end (to_chars with the general format and a precision does not give the printf %g output #1531).snprintfandfprintfread the flags, the width, the precision, the modifier and the conversion character, and stop after it. Each value is formatted into a buffer of the length that the newdetail::to_chars_lengthgives. When the output does not fit,snprintfwrites what fits and returns the full length, asstd::snprintfdoes. A missing value, or a width, precision or count aboveINT_MAX, returns -1 (snprintf writes past the buffer and does not read the conversion specification correctly #1532).operator<<takes the format, the precision and the flags from the stream. It writes the value with the fill and the grouping of the stream locale, asnum_putdoes. A value that does not fit the local buffer goes on the heap. An exception during the output setsbadbit(operator<< ignores the stream flags and writes past its buffer for a long output #1533).std::formatsupport is enabled again in C++20, becauseformat.hppincludes<version>first. Thestd::formatand{fmt}formatters share one parser and one writer in the newdetail/format_render.hpp. The 0 flag pads with zeros after the sign.Luses the locale of the format context, before or after the type. A bad specification throwsformat_error(std::format of a decimal does not compile in C++20, and the formatters ignore the 0 flag and the locale of the context #1534).locale_conversion.hppandu256.hppare removed, and the code that needed them is changed so that it gives no warning (locale_conversion.hpp turns off -Wtype-limits for all the code after the include #1535).test/github_issue_1530.cpptotest/github_issue_1534.cpp. locale_conversion.hpp turns off -Wtype-limits for all the code after the include #1535 has no test file.Fixes #1530
Fixes #1531
Fixes #1532
Fixes #1533
Fixes #1534
Fixes #1535
Correctness
/W4 /WX.to_charscalls, and the exact value for 20M random integers.Speed
The 128-bit print makes
to_charsofdecimal128_tanddecimal_fast128_tat full precision 34% to 48% faster with GCC and Clang, in the general, scientific and fixed formats. For example,decimal_fast128_tscientific goes from 93 ns to 54 ns.