How fmtlib Implements Range and Container Formatting in ranges.h

fmtlib detects range types at compile time using SFINAE-based traits in include/fmt/ranges.h, then dispatches to specialized formatter implementations that handle sequences, associative containers, and strings with customizable delimiters.

The {fmt} library provides comprehensive support for formatting C++ ranges and containers through a sophisticated template metaprogramming system centralized in include/fmt/ranges.h. This header implements a multi-layered approach that first classifies types using compile-time introspection, then selects appropriate formatting strategies via the range_format enumeration. Understanding how fmtlib implements range and container formatting in ranges.h reveals a design that balances extensibility with zero-overhead abstraction.

Detecting Range Types with SFINAE Traits

The foundation of fmtlib's range support rests on detecting whether a type provides begin and end iterators. The library defines several helper traits to probe for these operations across different interface styles.

Member function detection uses has_member_fn_begin_end_t to identify types defining begin() and end() as methods. For ADL-discoverable functions, the library provides has_const_begin_end and has_mutable_begin_end to handle both const-qualified and mutable iteration patterns (lines 62-90).

These individual checks compose into the primary range identification trait:

// Simplified representation of the detection logic
detail::is_range_<T>::value  // true if T implements the range interface

This trait aggregates the begin/end detection results to mark a type as iterable at compile time, enabling the formatter dispatch system to recognize containers like std::vector, std::list, or custom range types.

Excluding Tuples and Optional-Like Types

Not all types with begin/end semantics should use the generic range formatter. The library defines exclusion traits to redirect tuples and optional types to their own specialized formatters.

The is_tuple_like_ trait checks for std::tuple_size and std::tuple_element support, identifying std::tuple, std::pair, and std::array types. Meanwhile, is_optional_like_ detects types providing has_value() and value() member functions, capturing std::optional and similar monadic types (lines 22-38).

These exclusions ensure that code like fmt::print("{}", std::make_tuple(1, 2)) produces (1, 2) rather than [1, 2], maintaining intuitive output formats for distinct C++ idioms.

Classifying Range Formats with range_format_kind_

Once a type validates as a range, fmtlib categorizes it using range_format_kind_ to determine the appropriate formatting strategy. This metafunction maps types to the range_format enumeration, which defines five distinct categories (lines 52-60):

  • range_format::sequence – Generic iterable containers like vectors and lists
  • range_format::map – Associative containers such as std::map and std::unordered_map
  • range_format::set – Unique-key containers like std::set
  • range_format::string – Character ranges that should display as plain text
  • range_format::debug_string – Character ranges requiring quoted debug output

This classification drives template specialization selection, ensuring that a std::map<std::string, int> formats as {key: value} while a std::vector<int> formats as [1, 2, 3].

The range_formatter Class Template

For types classified as range_format::sequence, the library instantiates range_formatter, a class template that iterates through elements and applies their respective formatters. This implementation resides in the primary ranges header and provides extensive customization capabilities (lines 83-95).

The formatter supports runtime customization through method chaining:

std::vector<int> v = {1, 2, 3};

// Custom brackets and separator
fmt::print("{:}", fmt::formatter<std::vector<int>>()
                     .set_brackets("<", ">")
                     .set_separator(" | ")
                     .format(v, fmt::format_context{}));
// Output: <1 | 2 | 3>

For debug output, the ?s specifier instructs the formatter to quote string-like ranges, distinguishing the container boundaries from the string content.

Specialized Handling for Maps and Strings

Associative containers receive specialized treatment when range_format_kind<R,Char>::value == range_format::map. The dedicated formatter<R,Char> specialization iterates through std::pair<const Key, T> elements, formatting each as key: value inside braces { } rather than brackets. The n format specifier suppresses these delimiters entirely, producing flat output for parsing-friendly scenarios (lines 35-50).

String-like ranges bypass the generic iteration logic. When detected as range_format::string or range_format::debug_string, formatting forwards directly to formatter<std::basic_string<Char>>. For debug strings, the implementation adds surrounding quotes while escaping internal characters, providing Python-style repr() functionality (lines 92-100).

std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
fmt::print("map = {}\n", m);        // map = {a: 1, b: 2}

std::string s = "hello";
fmt::print("{:?s}\n", s);           // "hello"

Join Views and Container Adaptor Support

Beyond fixed containers, fmtlib provides fmt::join for formatting iterator ranges with custom separators. The join_view class created by this function stores an iterator pair or container reference along with a separator string. Its associated formatter iterates through the range once, inserting the separator between elements without allocating intermediate strings (lines 122-140):

std::vector<int> v = {1, 2, 3};
fmt::print("joined: {}\n", fmt::join(v, " + "));
// Output: joined: 1 + 2 + 3

Container adaptors like std::stack, std::queue, and std::priority_queue present special challenges because they obscure their underlying containers. The is_container_adaptor trait detects these types by checking for a nested container_type member. When identified, the formatter delegates to a view of the underlying container, providing visibility into adaptor contents without breaking encapsulation (lines 70-84).

Summary

  • Type Detection: The library uses has_member_fn_begin_end_t, has_const_begin_end, and has_mutable_begin_end to identify range interfaces, aggregated by detail::is_range_.
  • Format Classification: range_format_kind_ categorizes ranges into sequences, maps, sets, or strings via the range_format enumeration.
  • Generic Implementation: range_formatter handles sequences with customizable brackets and separators via set_brackets() and set_separator().
  • Specialized Outputs: Maps format as {key: value}, strings forward to dedicated string formatters, and debug mode adds quotes with escaping.
  • Utility Features: fmt::join creates lightweight views for custom separators, while is_container_adaptor enables formatting of std::stack and similar adaptors.

Frequently Asked Questions

How does fmtlib determine if a custom type is a range?

fmtlib checks for begin() and end() methods using has_member_fn_begin_end_t, or ADL-discoverable begin/end functions via has_const_begin_end and has_mutable_begin_end. These traits compose into detail::is_range_<T>, which evaluates to true if the type provides the standard range interface.

What distinguishes range_format::map from range_format::sequence?

range_format::map triggers a specialized formatter that expects std::pair elements and outputs them as key: value wrapped in braces { }, whereas range_format::sequence uses the generic range_formatter that outputs comma-separated values inside brackets [ ].

Can I customize the output format for a standard vector?

Yes. The range_formatter provides set_brackets() and set_separator() methods to customize delimiters. For one-off formatting with different separators, use fmt::join(vec, " | ") instead of formatting the container directly.

How does fmtlib format container adaptors like std::stack?

The library detects adaptors using is_container_adaptor, which checks for a container_type member typedef. When found, fmtlib accesses the underlying container through a view and applies standard range formatting, since adaptors themselves do not expose iterators directly.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →