`basic_format_arg` and `basic_format_args` in fmtlib: Core Type-Erased Argument Handling Explained
basic_format_arg stores a single type-erased formatting argument with runtime type information, while basic_format_args provides a lightweight, non-owning view of an entire argument collection—together they enable the {fmt} library to format any C++ types efficiently without template explosion.
The {fmt} library (fmtlib/fmt) implements one of the fastest C++ formatting engines available, and these two template classes sit at its heart. In include/fmt/core.h, basic_format_arg<Context> and basic_format_args<Context> provide the mechanism that allows fmt::format to accept any number of arbitrarily typed arguments while maintaining zero-overhead abstraction.
What basic_format_arg Does
basic_format_arg<Context> is a lightweight value type that wraps one formatting argument together with its runtime type descriptor.
Internal Structure
At its core, the class contains two members:
detail::value<Context> value_— holds the actual data (inline or via pointer)detail::type type_— an enum identifying whether the value is anint,double,string_view, custom type, etc.
This pairing is what makes type erasure possible. The formatting engine does not need to know the C++ type at compile time; it inspects type_ and dispatches to the correct handler via the visit method.
How You Encounter It
You rarely construct basic_format_arg directly. Instead, fmt::make_format_args(...) builds a temporary array of these objects behind the scenes. Each argument in your format call becomes one basic_format_arg instance.
The class exposes visit(F f) as its primary interface. This method takes a callable and invokes the correct overload based on the stored type_, enabling generic processing of any argument type.
What basic_format_args Does
basic_format_args<Context> represents the entire argument list as a non-owning view. It is what functions like vformat and vprint actually receive.
Key Design Decisions
- Non-owning: It stores a pointer to an array of
basic_format_argplus a descriptor (desc_), not the data itself - Packed vs. unpacked: The descriptor indicates whether arguments are stored in a compact, compile-time-optimized format or a dynamic array
- Indexable:
type(int index)returns the type of the argument at that position without accessing the value
This design allows passing arbitrarily many arguments through a single function parameter with no allocation and minimal indirection.
The Role in Type-Erased APIs
When you call fmt::format("{}", value), the library internally routes to vformat(string_view, format_args), where format_args is an alias for basic_format_args<format_context>. This indirection layer is what enables:
- Separate compilation of format string parsing from argument formatting
- Runtime format strings (when compile-time checks are disabled)
- Building argument lists incrementally before formatting
How They Work Together
The typical flow through the {fmt} codebase looks like this:
#include <fmt/core.h>
#include <iostream>
int main() {
// Step 1: Build type-erased argument storage
fmt::format_args args = fmt::make_format_args(10, 2.5, "hello");
// Step 2: Pass to type-erased formatting function
std::string result = fmt::vformat("{} {} {}", args);
std::cout << result << '\n'; // "10 2.5 hello"
}
Under the hood:
make_format_args(10, 2.5, "hello")constructs threebasic_format_argobjects, one for each value- These are stored in a temporary array wrapped by
basic_format_args vformatreceives this view and iterates through arguments by index, callingvisiton each to emit formatted output
You can also inspect individual arguments directly:
const fmt::basic_format_arg<fmt::format_context>& a0 = args.arg(0);
auto value = a0.visit([](auto v) -> int {
return static_cast<int>(v);
});
Source File Locations
| File | Purpose |
|---|---|
include/fmt/core.h |
Defines basic_format_arg and basic_format_args templates with their visit methods and type enum |
include/fmt/format.h |
Implements vformat, vprint, and the core formatting engine that consumes these types |
include/fmt/args.h |
Provides dynamic_format_arg_store for building argument lists at runtime |
The implementation in core.h uses careful layout optimizations: basic_format_arg is typically 16 bytes on 64-bit platforms, small enough to pass by value efficiently, while basic_format_args adds only pointer and descriptor overhead regardless of argument count.
Summary
basic_format_argwraps one value plus its runtime type, enabling uniform treatment of any formattable typebasic_format_argsprovides a lightweight, iterable view of an argument array without copying data- Together they separate the static, compile-time world of template argument packs from the dynamic, runtime world of format string interpretation
- The design enables both high performance (no virtual calls, minimal indirection) and flexibility (runtime format strings, custom type support)
Frequently Asked Questions
What is the difference between format_args and basic_format_args?
format_args is simply a type alias for basic_format_args<format_context>, defined in include/fmt/core.h. The basic_ prefix follows C++ standard library naming conventions for allocator-aware templates, though {fmt} does not expose allocator customization for format arguments. You use format_args in virtually all application code.
Why does {fmt} use type erasure instead of pure templates?
Pure template approaches instantiate formatting logic for every unique combination of argument types, causing binary bloat. By erasing to basic_format_arg early, {fmt} compiles format string parsing once and dispatches to type-specific formatters through the visit mechanism. This keeps code size small while preserving performance—measured overhead is typically a single indirect call per argument.
Can I create basic_format_args without make_format_args?
Advanced use cases can construct basic_format_args from a dynamic_format_arg_store (in include/fmt/args.h) or manually from a basic_format_arg array. However, this requires internal knowledge of the desc_ encoding and is not recommended for typical applications. The public API through make_format_args handles all common needs safely.
How does visit avoid virtual function overhead?
visit uses a switch statement on the type_ enum to dispatch to the correct handler, not virtual functions. This allows inlining of the visitor's body and eliminates the vtable pointer overhead. The implementation in core.h ensures the compiler can optimize across the dispatch, achieving performance comparable to direct function calls.
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 →