How to Format C++ Ranges and Tuples with fmtlib: A Complete Guide

fmtlib automatically formats any range or tuple-like type when you include <fmt/ranges.h>, using compile-time traits fmt::is_range<T> and fmt::is_tuple_like<T> to select dedicated formatter specializations.

The {fmt} library provides first-class support for formatting C++ ranges and tuples without requiring manual iteration or string concatenation. By including include/fmt/ranges.h, you enable automatic detection and formatting of standard containers, arrays, pairs, and user-defined types that expose begin/end or std::tuple_size. This capability is implemented through specialized formatter templates that handle bracket delimiters, separators, and nested element formatting.

Understanding fmtlib Range and Tuple Detection

The formatting engine distinguishes between ranges and tuple-like types using compile-time traits defined in include/fmt/ranges.h.

Detecting Tuple-Like Types

The library identifies tuple-like structures through the presence of std::tuple_size<T>::value. The internal predicate detail::is_tuple_like_ performs this check, which covers std::tuple, std::pair, std::array, and user-defined tuple-compatible structs. The public alias fmt::is_tuple_like<T> (lines 89-93) forwards to this implementation, enabling the selection of a dedicated formatter when every element is itself formattable (fmt::is_tuple_formattable<T>).

Detecting Range Types

A type qualifies as a range when detail::range_begin and detail::range_end can be invoked successfully. This includes C-arrays, containers with member functions, or types found through argument-dependent lookup. The trait fmt::is_range<T> (lines 62-68) combines these checks while excluding optional-like types and objects that expose to_string_view overloads, preventing ambiguity with string formatting.

Formatting Tuple-Like Types

When fmt::is_tuple_like<T> evaluates to true, the library instantiates a specialized formatter defined around lines 3000-3030 in include/fmt/ranges.h.

The Tuple Formatter Specialization

The formatter constructs a compile-time array of element formatters using detail::tuple::get_formatters. During parsing, parse() forwards the format specification to each element formatter via detail::parse_empty_specs. The actual format() operation writes the configured opening bracket, iterates through elements using detail::for_each2, inserts the separator between values, and appends the closing bracket through the write_body method (lines 3015-3023).

auto tup = std::make_tuple(42, 3.14, "abc");
fmt::print("Default: {}\n", tup);                 // (42, 3.14, abc)
fmt::print("Custom brackets: {:#}\n", tup);      // {42, 3.14, abc}
fmt::print("Custom sep: {:;}\n", tup);           // (42; 3.14; abc)

Customizing Tuple Output

You can modify the default parentheses and comma separator at runtime using set_brackets() and set_separator() (lines 30-34). These methods allow dynamic reconfiguration of the formatter instance when you need different delimiters for specific output contexts.

Formatting Range Types

For ranges where the element type satisfies formattability requirements, the library provides a generic formatter specialization located at lines 3600-3660.

The Range Formatter Specialization

The range formatter stores an underlying element formatter of type detail::range_formatter_type. It iterates over the range in write_body (lines 4990-5015), formatting each element and inserting the configured separator, then surrounds the result with square brackets by default.

Range Format Specifiers

The parse() method (lines 4440-4490) recognizes several format options:

  • n — Suppresses brackets entirely, outputting only the comma-separated elements
  • s — Applies string-style quoting to the output
  • ? — Enables debug output formatting
std::vector<int> vec = {1, 2, 3};
fmt::print("Default: {}\n", vec);                // [1, 2, 3]
fmt::print("No brackets: {:n}\n", vec);          // 1, 2, 3
fmt::print("Debug string: {:?s}\n", vec);        // "1, 2, 3"

Using fmt::join for Custom Separators

For both ranges and tuple-like objects, the fmt::join view (lines 8420-8665) formats elements with a custom separator without constructing intermediate strings or requiring bracket delimiters. This is more efficient than creating temporary strings when you only need flat concatenation.

fmt::print("Join view: {}\n", fmt::join(vec, " | ")); // 1 | 2 | 3
fmt::print("Join tuple: {}\n", fmt::join(tup, "-"));   // 42-3.14-abc

The fmt::join function works with C-arrays, standard containers, and tuple-like types, accepting any formattable range as its first argument.

Summary

  • Include <fmt/ranges.h> to enable automatic formatting of ranges and tuples, as documented in the repository README (line 129).
  • Tuple detection relies on std::tuple_size via detail::is_tuple_like_, while range detection uses detail::range_begin/range_end through the fmt::is_range<T> trait.
  • Default formatting uses parentheses () for tuples and square brackets [] for ranges, both with ", " as the element separator.
  • Runtime customization allows changing brackets and separators via set_brackets() and set_separator() on the formatter instance.
  • Format specifiers provide control over range output: {:n} removes brackets, {:s} adds quoting, and {:?} provides debug formatting.
  • Use fmt::join when you need custom separators without brackets for either ranges or tuples.

Frequently Asked Questions

Why do I get a compilation error when trying to format a vector?

You must include include/fmt/ranges.h in addition to the standard fmt/format.h. According to the fmtlib source code, range formatting is provided as an extension module and is not available in the core formatting header. The README explicitly mentions this requirement at line 129.

How does fmtlib distinguish between a range and a tuple?

The library uses separate compile-time traits: fmt::is_tuple_like<T> checks for std::tuple_size<T>::value (lines 89-93), while fmt::is_range<T> checks for valid begin/end expressions via detail::range_begin and detail::range_end (lines 62-68). A type can theoretically satisfy both, but tuple formatting takes precedence when std::tuple_size is present.

Can I format nested containers like vector of tuples?

Yes. The formatters in include/fmt/ranges.h recursively check formattability of element types. The tuple formatter's detail::tuple::get_formatters constructs formatters for each element, while the range formatter uses detail::range_formatter_type for its elements. Both mechanisms support arbitrary nesting depths as long as all constituent types are formattable.

What is the performance difference between fmt::join and standard range formatting?

fmt::join (lines 8420-8665) avoids constructing the opening and closing brackets and skips the write_body overhead of the standard formatter. It directly iterates the range or tuple elements, inserting only the specified separator. This makes it more efficient when you need simple delimited output without structural brackets, as it eliminates the branching and bracket-tracking logic found in the standard write_body implementations (lines 3015-3023 and 4990-5015).

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 →