C++ Logo

std-discussion

Advanced search

std::format: Can't use a flag for floating-point and get the default format

From: Rob Lefebvre <rob_lefebvre_at_[hidden]>
Date: Sat, 6 Feb 2021 18:16:08 +0000
Note: We are using the fmtlib implementation of std::format.

We have coders who prefer to tag all std::format() arguments with types. For example:

std::format("{:d} {:g}", myIntValue, myDoubleValue);

They do this for both clarity as well as compile-time sanity type checking.

However, this does not work well for floating point because there is no type that represents the same thing as <none>. From https://en.cppreference.com/w/cpp/utility/format/formatter#Standard_format_specification:

? e: Produces the output as if by calling std::to_chars(first, last, value, std::chars_format::scientific, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision, or 6 if precision is not specified.
? f, F: Produces the output as if by calling std::to_chars(first, last, value, std::chars_format::fixed, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision, or 6 if precision is not specified.
? g: Produces the output as if by calling std::to_chars(first, last, value, std::chars_format::general, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision, or 6 if precision is not specified.
? none: If precision is specified, produces the output as if by calling std::to_chars(first, last, value, std::chars_format::general, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision; otherwise, the output is produced as if by calling std::to_chars(first, last, value)<https://en.cppreference.com/w/cpp/utility/to_chars>.

Additionally, <none> has a "round-trip" guarantee that is not available in the other formats. This is guaranteed to be true for all double v:

v == std::stod(std::format("{}",v));

Some reference examples:

std::format("{}", 0.0); 0.0
std::format("{}", 123456789123456789123456789123456789.0); 1.2345678912345678e+35
std::format("{}", 1.23456789123456789123456789123456789); 1.234567891234568
std::format("{}", 1.23); 1.23
std::format("{}", -14.3); -14.3
std::format("{}", 1.0); 1.0
std::format("{}", -14.0); -14.0
std::format("{}", 1e20); 1e+20
std::format("{}", 1.49e18); 1.49e+18
std::format("{}", 1e-8); 1e-08
std::format("{}", 1.23456789123456789123456789e-8); 1.234567891234568e-08
std::format("{}", -3e20); -3e+20

Additionally background: Our company writes software for electronic circuit simulation and measurement. Our very large code base code of varying age, some of which is over 40 years old (simple generation as the original Berkeley Spice). Precision of output is critical, as is being very careful while we refactor. Compile-time checks are key. Those compile-time checks are not nearly so robust when using {}.

PROPOSAL:

I propose a change be made to format syntax allowing an explicit way to produce output "as if by calling std::to_chars(first, last, value)<https://en.cppreference.com/w/cpp/utility/to_chars>".

The following options would accomplish the change, ordered descending by my personal preference:

Possibility #1: Change the definition of type g and none:

g: If precision is specified, produces the output as if by calling std::to_chars(first, last, value, std::chars_format::general, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision; otherwise, the output is produced as if by calling std::to_chars(first, last, value)<https://en.cppreference.com/w/cpp/utility/to_chars>.

none: same as g.

This possibility seems to make the most sense: g is a "generalized" format, intended long ago to be the best possible format. Having it default to the required precision based on the input would simply extend that concept.

Con: This breaks tradition with printf. It is also technically a breaking change to the C++20 spec, but since no vendor has yet created std::format, "breaking change" isn't as meaningful.

Possibility #2: Add a new type z and change none:

z: If precision is specified, produces the output as if by calling std::to_chars(first, last, value, std::chars_format::general, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision; otherwise, the output is produced as if by calling std::to_chars(first, last, value)<https://en.cppreference.com/w/cpp/utility/to_chars>.

none: same as z.

(Of course, any unused letter could be chosen.)

This possibility is the simplest without any ripple to existing code, but it does steal an additional letter. It also adds some redundancy with g.


Possibility #3: Allow {:.g} to be legal, meaning "required" precision.

g: Produces the output as if by calling std::to_chars(first, last, value, std::chars_format::general, precision)<https://en.cppreference.com/w/cpp/utility/to_chars> where precision is the specified precision, or 6 if precision is not specified; Otherwise, if a blank precision is specified, , the output is produced as if by calling std::to_chars(first, last, value)<https://en.cppreference.com/w/cpp/utility/to_chars>

This possibility doesn't solve the problem as cleanly, but it would probably work.

There are likely possibilities which could be proposed by responders.

Thank you,

Rob Lefebvre
Senior Software Architect, Keysight Technologies

Received on 2021-02-06 12:16:17