How fmtlib Handles Locale-Dependent Formatting with Thousands Separators
fmt formats numbers according to a std::locale when the locale-aware 'n' format specifier is used, wrapping the locale in a lightweight locale_ref and applying digit grouping rules via the digit_grouping class.
The fmt library provides efficient, type-safe formatting while avoiding the bloat of standard C++ iostreams. When you require locale-dependent formatting with thousands separators, the library employs a specialized three-stage pipeline that lazily loads locale data only when the 'n' specifier is requested, minimizing compile-time overhead from the <locale> header.
The Three-Stage Locale Formatting Pipeline
The implementation avoids pulling the heavy <locale> header into every translation unit by deferring locale-specific operations until a localized format specifier triggers them.
Stage 1: Lightweight Locale Abstraction via locale_ref
At the entry point, fmt wraps the std::locale in a locale_ref object. This lightweight wrapper, implemented in include/fmt/format.h around line 2102, stores a reference to the locale without forcing template instantiations that depend on standard locale facilities. The locale_ref propagates through the formatting pipeline, carrying the locale context to downstream formatting functions without introducing heavyweight dependencies in header files.
Stage 2: Extracting Thousands Separators with thousands_sep_impl
When localized number formatting is required, the library calls thousands_sep_impl, defined in src/format.cc at line 45. This template function accesses the std::numpunct facet from the wrapped locale to retrieve two critical pieces of data:
- The thousands separator character (e.g.,
,for US English,.for German, or a non-breaking space for French) - The grouping pattern (a
std::stringspecifying digit group sizes, such as"3"for standard thousands or"3;2"for the Indian numbering system)
The function returns a thousands_sep_result<Char> structure containing these values. This result is cached inside a digit_grouping object, constructed in include/fmt/format.h between lines 2112 and 2125, ensuring the locale facet lookup happens only once per formatting operation.
Stage 3: Applying Digit Grouping in digit_grouping::put
The actual insertion of separators occurs in digit_grouping::put, implemented in include/fmt/format-inl.h from lines 2160 to 2189. This method walks the number's digits from least-significant to most-significant, inserting the separator character whenever the grouping pattern mandates a boundary. If the locale specifies the classic "C" locale (the default), the grouping pattern is empty and no separators are inserted.
Using Locale-Aware Formatting in Practice
To format numbers with thousands separators, use the {:n} specifier combined with fmt::locale. The default std::locale::classic() produces unlocalized output, while specific locales inject their native separators automatically:
#include <fmt/format.h>
#include <locale>
int main() {
// Classic (C) locale – no separator.
fmt::print("{:n}\n", 1234567); // → 1234567
// US English locale – comma as thousands separator.
fmt::print(fmt::locale("en_US.UTF-8"), "{:n}\n", 1234567);
// → 1,234,567
// German locale – period as thousands separator.
fmt::print(fmt::locale("de_DE.UTF-8"), "{:n}\n", 1234567);
// → 1.234.567
// French locale – non‑breaking space as separator.
fmt::print(fmt::locale("fr_FR.UTF-8"), "{:n}\n", 1234567);
// → 1 234 567
}
The fmt::locale helper constructs a locale_ref from the std::locale and forwards it into the formatting functions, enabling the three-stage pipeline described above.
Key Source Files and Implementation References
The locale-dependent formatting logic spans three critical files in the fmtlib/fmt repository:
-
include/fmt/format.h– Contains thelocale_refclass definition, thedigit_groupingconstructor (lines 2112–2125), and the public API entry points forformatandvformat. -
src/format.cc– Housesthousands_sep_impl(line 45) anddecimal_point_impl, which extract the separator characters and grouping patterns from thestd::numpunctfacet. -
include/fmt/format-inl.h– Implementsdigit_grouping::put(lines 2160–2189), the low-level algorithm that inserts thousands separators according to the locale's digit grouping rules.
Summary
fmtemploys a lazy-loading architecture wherelocale_refwrapsstd::localewithout pulling<locale>into every translation unit.- The
'n'format specifier triggers locale-aware formatting, extracting separator characters and grouping patterns viathousands_sep_implinsrc/format.cc. - Digit grouping logic resides in
digit_grouping::putwithininclude/fmt/format-inl.h, processing numbers from least-significant to most-significant digit. - Default behavior uses
std::locale::classic(), producing no separators; specific locales enable comma, period, or space separators automatically.
Frequently Asked Questions
How do I enable thousands separators in fmtlib?
Use the n format specifier inside your format string (e.g., {:n}). To apply locale-specific separators, pass a fmt::locale object as the first argument to fmt::print or fmt::format, such as fmt::format(fmt::locale("en_US.UTF-8"), "{:n}", 1234567).
What is the default locale behavior in fmtlib?
By default, fmt uses std::locale::classic() (the "C" locale), which results in no thousands separators being inserted. The number is formatted as a plain digit string regardless of the n specifier unless you explicitly provide a different locale via fmt::locale.
Where does fmtlib store the thousands separator character?
The separator character and grouping pattern are stored in a thousands_sep_result<Char> structure returned by thousands_sep_impl. This result is cached within the digit_grouping class, instantiated in include/fmt/format.h and populated from the std::numpunct facet defined in src/format.cc.
Does fmtlib require the <locale> header for all translation units?
No. fmt avoids including <locale> in headers through the locale_ref abstraction. The heavy locale headers are only processed in src/format.cc where thousands_sep_impl is defined, keeping compile times minimal for code that does not use locale-dependent formatting.
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 →