How to Get Automatic Floating-Point Precision in C++ std::format (Fixed Mode)
When transitioning to Modern C++ (C++20 and beyond), developers often expect std::format to replace older formatting mechanisms completely. However, you might run into an unexpected surprise when printing floating-point numbers in fixed-point mode:
#include <format>
#include <iostream>
int main() {
constexpr double num = 2.000000007;
std::cout << std::format("{:f}", num) << '\n'; // Outputs: 2.000000
}
While std::to_chars with std::chars_format::fixed automatically outputs the exact number of decimal places needed to represent the value (2.000000007), std::format("{:f}", num) truncates it to 6 decimal places. Why does this happen, and how can you achieve automatic precision in fixed mode?
Why Does std::format Default to 6 Decimal Places?
The design of std::format specification borrows heavily from Python's str.format() and C's printf. In the C++ standard (specifically [format.string.std]), floating-point presentation types follow these rules:
forF(Fixed): If no precision is specified, it defaults to 6.eorE(Scientific): Default precision is 6.- Empty specifier
{}: Produces the shortest decimal representation that accurately round-trips back to the original value using the shortest representation algorithm (similar to Ryu or Dragonbox).
Because {:f} explicitly requests fixed notation, the standard mandates a default precision of 6. Unfortunately, the format string syntax does not have a flag that means "fixed notation with automatic shortest precision."
Solution 1: Use the Default Format Specifier {} (General Representation)
If you omit the format specifier or use {}, std::format automatically prints the exact precision required:
#include <format>
#include <iostream>
int main() {
constexpr double num = 2.000000007;
std::cout << std::format("{}", num) << '\n'; // Outputs: 2.000000007
}
Caveat: The default specifier behaves like std::chars_format::general. For numbers that are very small (e.g., < 1e-4) or very large (e.g., ≥ 1e16), it automatically flips to scientific notation (e.g., 1e-05 instead of 0.00001). If you must enforce fixed notation across all magnitudes, this solution alone is not enough.
Solution 2: Create a Lightweight Fixed Wrapper Using std::to_chars
Since std::to_chars already supports the exact behavior you need without dynamic allocations, the cleanest and most performant solution is to create a small wrapper type and provide a custom std::formatter specialization.
#include <charconv>
#include <format>
#include <iostream>
#include <string_view>
#include <system_error>
struct auto_fixed {
double value;
};
template <>
struct std::formatter<auto_fixed> : std::formatter<std::string_view> {
auto format(auto_fixed af, std::format_context& ctx) const {
char buffer[128];
auto [ptr, ec] = std::to_chars(buffer, buffer + sizeof(buffer), af.value, std::chars_format::fixed);
if (ec != std::errc()) {
return std::formatter<std::string_view>::format("error", ctx);
}
return std::formatter<std::string_view>::format(std::string_view(buffer, ptr), ctx);
}
};
int main() {
constexpr double num1 = 2.000000007;
constexpr double num2 = 0.000012345;
std::cout << std::format("{}", auto_fixed{num1}) << '\n'; // Outputs: 2.000000007
std::cout << std::format("{}", auto_fixed{num2}) << '\n'; // Outputs: 0.000012345
}
Advantages of this approach:
- Zero heap allocation: Uses a small stack buffer.
- Composable: Works seamlessly within larger format strings like
std::format("Coordinates: ({}, {})", auto_fixed{x}, auto_fixed{y}). - Consistent: Guarantees fixed-point output without dropping digits or switching to exponent notation.
Solution 3: Dynamic Precision with std::format
If you prefer to stay purely within the standard formatting library without wrapping types, you can dynamically determine the required precision using std::to_chars and pass it as an argument using the nested {:.{}f} syntax:
#include <charconv>
#include <format>
#include <iostream>
#include <string_view>
int get_fixed_precision(double val) {
char buffer[128];
auto [ptr, ec] = std::to_chars(buffer, buffer + sizeof(buffer), val, std::chars_format::fixed);
std::string_view sv(buffer, ptr);
auto dot_pos = sv.find('.');
return (dot_pos == std::string_view::npos) ? 0 : static_cast<int>(sv.size() - dot_pos - 1);
}
int main() {
double num = 2.000000007;
int precision = get_fixed_precision(num);
std::cout << std::format("{:.{}f}", num, precision) << '\n'; // Outputs: 2.000000007
}
While this works, Solution 2 is generally preferred because calling to_chars just to count decimal positions and then running std::format's floating-point routine introduces redundant computations.
Summary
Unlike std::to_chars, std::format("{:f}", val) adheres to standard C-style precision rules where omitted precision defaults to 6. If your values stay within normal ranges, the default format std::format("{}", val) provides shortest round-trip output. For guaranteed fixed-point formatting with exact precision, wrapping std::to_chars inside a custom std::formatter is the most robust and idiomatic C++ solution.