# `basic_format_arg` and `basic_format_args` in fmtlib: Core Type-Erased Argument Handling Explained

> Understand basic_format_arg and basic_format_args in fmtlib for efficient, type-erased argument handling and formatting of any C++ type without template bloat.

- Repository: [Hello World Foundation/fmt](https://github.com/fmtlib/fmt)
- Tags: internals
- Published: 2026-09-08

---

**`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`](https://github.com/fmtlib/fmt/blob/main/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 an `int`, `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_arg` plus 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:

```cpp
#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:

1. `make_format_args(10, 2.5, "hello")` constructs three `basic_format_arg` objects, one for each value
2. These are stored in a temporary array wrapped by `basic_format_args`
3. `vformat` receives this view and iterates through arguments by index, calling `visit` on each to emit formatted output

You can also inspect individual arguments directly:

```cpp
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`](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h) | Defines `basic_format_arg` and `basic_format_args` templates with their `visit` methods and `type` enum |
| [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) | Implements `vformat`, `vprint`, and the core formatting engine that consumes these types |
| [`include/fmt/args.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/args.h) | Provides `dynamic_format_arg_store` for building argument lists at runtime |

The implementation in [`core.h`](https://github.com/fmtlib/fmt/blob/main/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_arg`** wraps one value plus its runtime type, enabling uniform treatment of any formattable type
- **`basic_format_args`** provides 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`](https://github.com/fmtlib/fmt/blob/main/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`](https://github.com/fmtlib/fmt/blob/main/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`](https://github.com/fmtlib/fmt/blob/main/core.h) ensures the compiler can optimize across the dispatch, achieving performance comparable to direct function calls.