How to Format long double with Locale-Aware Decimal in fmtlib
Use the L length modifier with a std::locale argument in fmt::format to emit locale-specific decimal separators for long double values.
In the fmtlib/fmt repository, formatting extended-precision floating-point numbers according to regional conventions requires understanding how the library delegates punctuation rules to the C++ locale system. The formatting engine uses a type-erased locale_ref wrapper to pass locale information through the template-heavy internals without heavy header dependencies. When you supply a std::locale to the formatting functions, the library queries that locale for decimal separators via internal helper functions before writing the digits.
How Locale Propagation Works in fmtlib
The formatting pipeline begins in include/fmt/core.h, which defines locale_ref. This lightweight abstraction wraps a std::locale pointer and allows the formatting engine to access locale data only when needed, avoiding the cost of including the heavy <locale> header in every translation unit.
When formatting a long double, the value flows through detail::write<Char>(out, value, specs, loc) defined in include/fmt/format.h. This function receives the locale_ref object extracted from the format_context. Inside the write routine, the library calls decimal_point_impl (lines 1343–1348) to determine whether to use a comma, period, or other character as the decimal mark. Similarly, thousands_sep_impl (lines 1331–1335) retrieves the grouping separator when number formatting is requested.
The Long Double Formatter Specialization
Unlike standard double, the long double type requires explicit handling through the length modifier system. In include/fmt/format.h around lines 4220–4224, the parser recognizes the L specifier and instantiates detail::native_formatter for the underlying detail::float128 or long double type.
This specialization ultimately invokes detail::write<Char>(out, value, specs, loc), forwarding the locale reference so that decimal_point_impl can emit the correct punctuation. Without the L modifier, the formatter treats the value as a standard floating-point type and may truncate precision or ignore extended exponent ranges.
Formatting long double with Locale
To obtain locale-aware output, follow these steps:
- Construct a
std::localeobject representing your target region. - Pass the locale as the first argument to
fmt::format,fmt::format_to, orfmt::vformat. - Include the
Llength modifier in the format string. Combine with thenoption for thousands separators.
#include <fmt/format.h>
#include <locale>
int main() {
// German locale uses comma as decimal separator
std::locale german("de_DE.UTF-8");
long double val = 1234.5678L;
// Locale-aware decimal point only
std::string s1 = fmt::format(german, "{:L}", val);
// Result: "1234,5678"
// Combining with 'n' for thousands separator
std::string s2 = fmt::format(german, "{:nL}", val);
// Result: "1 234,5678" (non-breaking space as thousands separator)
}
If you omit the locale argument, fmtlib falls back to the classic "C" locale, which always uses a period (.) as the decimal separator regardless of the platform or system settings.
Key Implementation Files
include/fmt/format.h: Containsdecimal_point_impl,thousands_sep_impl, and theformatterspecializations for floating-point types includinglong double.include/fmt/core.h: Defineslocale_ref, the type-erased wrapper that transports locale data through the formatting pipeline without forcing<locale>inclusion.include/fmt/printf.h: Demonstrates howprintf-style APIs reuse the samewritefunction and locale-aware helpers, confirming the unified architecture.
Summary
- Pass a
std::localeas the first argument tofmt::formatto enable locale-aware punctuation. - Use the
Lmodifier to ensurelong doublevalues route through the correctdetail::native_formatterspecialization. - Combine with
nfor full number formatting that includes thousands separators and the locale-specific decimal point. - Source implementation resides in
include/fmt/format.hlines 1331–1348 for separator logic and lines 4220–4224 for the formatter specialization.
Frequently Asked Questions
What is the difference between the L and n format specifiers?
The L length modifier tells the parser to treat the argument as long double (or float128 on supported platforms) and route it through the extended-precision formatter. The n option enables locale-aware number formatting, which includes both thousands separators and the locale-specific decimal separator. Use them together ({:nL}) for fully localized extended-precision output.
Do I need to link against special libraries for locale support?
No. The fmtlib locale integration relies entirely on the C++ standard library's <locale> header. You only need to ensure your system has the requested locale installed (e.g., de_DE.UTF-8) and that you construct the std::locale object before passing it to formatting functions.
Why does my output still show a period instead of a comma?
This occurs when you either omit the locale argument from fmt::format or pass a classic "C" locale. Without an explicit locale, fmtlib defaults to the C locale's convention of using . as the decimal separator. Always pass the locale object as the first parameter: fmt::format(my_locale, "{:L}", value).
Does this work for __float128 or platform-specific extended types?
Yes. The detail::native_formatter specialization handles both long double and detail::float128 (when FMT_USE_FLOAT128 is defined) using the same L modifier. The locale-aware write routine processes these types identically, querying decimal_point_impl for the correct separator regardless of the underlying floating-point width.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →