# How fmtlib Handles Arguments and Performs Type Erasure in C++

> Discover how fmtlib handles arguments and performs type erasure for uniform C++ type processing. Learn about its lightweight pipeline and tagged value array.

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

---

**`{fmt}` passes all formatting arguments through a lightweight type-erasure pipeline so the formatting engine can process heterogeneous C++ types uniformly using a compact, contiguous array of tagged values.**

The `fmtlib/fmt` library achieves high performance and type safety by decoupling argument storage from the formatting engine. When you call `fmt::format()`, your values are transformed into a portable, type-erased representation that the formatter traverses at runtime. This article examines the complete argument handling pipeline in the `{fmt}` source code, from ingestion to the final formatting call.

## The Three Core Components of Argument Handling

`{fmt}` implements type erasure through three tightly integrated classes, each defined in a specific header file.

### `basic_format_arg` — The Type-Erased Value Container

The fundamental unit of argument storage is **`basic_format_arg<Context>`**, defined in [`include/fmt/core.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h). This small object combines a **value union** with a **type tag** (`detail::type`) that tells the formatter how to interpret the stored bits.

The union inside `basic_format_arg` can hold:
- Integral types (`int_type`, `uint_type`, `long_long_type`, etc.)
- Floating-point values (`double_type`, `long_double_type`)
- Pointers and strings (`pointer_type`, `cstring_type`, `string_view_type`)
- Custom types via a `custom_value` handle
- Named argument references

When constructed, `basic_format_arg` receives the actual value (or pointer) plus a compile-time-deduced `detail::type` enum value. This tag completely replaces compile-time type information with a runtime identifier that the formatting engine checks when extracting values.

### `basic_format_args` — The Lightweight View

The **`basic_format_args<Context>`** class in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) provides a non-owning view over a contiguous array of `basic_format_arg` objects. It stores:
- A pointer to the first argument (`data_`)
- The argument count (`size_`)
- A boolean flag indicating named argument presence (`named_`)

This design follows the C++ Core Guidelines principle of separating ownership from access. The view is what low-level functions like `vformat()` and `format_to()` actually consume—no templates, no heavy machinery, just pointer arithmetic over type-tagged values.

### `dynamic_format_arg_store` — The Dynamic Builder

When argument count or types are unknown at compile time, **`dynamic_format_arg_store<Context>`** in [`include/fmt/args.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/args.h) builds the `basic_format_arg` array dynamically. This class manages two separate storage arenas:

- **`data_`** — a `std::vector<basic_format_arg<Context>>` holding the type-erased argument descriptors
- **`dynamic_args_`** — a `detail::dynamic_arg_list` linked list owning any heap-allocated objects that cannot fit directly in the `basic_format_arg` union

The store's critical responsibility is deciding whether each incoming argument needs **copying** or can be **referenced**.

## Copy vs. Reference: The `need_copy` Decision

Inside `dynamic_format_arg_store::push_back<T>()` (lines 78-90 of [`args.h`](https://github.com/fmtlib/fmt/blob/main/args.h)), the trait **`need_copy<T>`** makes the storage determination:

| Condition | Behavior |
|-----------|----------|
| `need_copy<T>::value == false` | Store reference directly in `basic_format_arg`; no heap allocation |
| `need_copy<T>::value == true` | Allocate `typed_node<T>` in `dynamic_args_` linked list, store reference in vector |

A copy is required for:
- Non-reference-wrapper types that aren't simple string views
- Custom types not already `basic_format_arg`-compatible
- Values that would dangle if referenced (temporary objects)

The `dynamic_arg_list` uses a node hierarchy where each `typed_node<T>` contains the actual storage plus type-erased deallocation logic. This lets `{fmt}` handle arbitrarily complex types while keeping the hot-path `basic_format_arg` vector compact and cache-friendly.

## The Complete Argument Pipeline

Here's how the pieces flow together in a typical formatting call:

1. **Argument ingestion** — `fmt::format("{:d} {}", 42, "hello")` instantiates `format_string<T...>` and forwards each argument through `detail::make_arg<T>()` via the `fmt::arg` helper.

2. **Type erasure** — Each `basic_format_arg` constructor receives the value and its `detail::type` tag (e.g., `type::int_type`, `type::cstring_type`). The original C++ type information is discarded; only the tag remains.

3. **View creation** — `dynamic_format_arg_store` exposes `operator basic_format_args<Context>() const`, which constructs a view pointing at `data_.data()`.

4. **Formatting execution** — `vformat()` or equivalent iterates the view, reads each argument's type tag, extracts the appropriate union member, and applies format specifications.

5. **Named argument handling** — When `fmt::arg("name", value)` is used, the store inserts a placeholder at index 0 and maintains a parallel `named_info_` vector mapping names to indices. The `named` flag in `basic_format_args` signals the formatter to resolve `{name}` references.

## Working with Type-Erased Arguments: Code Examples

### Basic Compile-Time Formatting

```cpp
#include <fmt/format.h>

// Type erasure happens automatically behind this simple API
std::string s = fmt::format("The answer is {} and {}", 42, 3.14);
// Result: "The answer is 42 and 3.14"

```

Even this straightforward call internally populates a `dynamic_format_arg_store`, converts to `basic_format_args`, and dispatches through `vformat`.

### Dynamic Store for Runtime Argument Counts

```cpp
#include <fmt/args.h>
#include <fmt/format.h>
#include <functional>  // std::cref

fmt::dynamic_format_arg_store<fmt::format_context> store;

store.push_back(42);                                   // copied
store.push_back(std::string("hello"));                 // copied (via node)
store.push_back(std::cref(s));                         // referenced, no copy

// Convert to view and format
std::string result = fmt::vformat("{} – {}", store);
// Result: "42 – hello"

```

### Named Arguments

```cpp
#include <fmt/args.h>
#include <fmt/format.h>

auto args = fmt::make_format_args(
    fmt::arg("x", 10),
    fmt::arg("y", std::string_view{"world"}));

std::string txt = fmt::vformat("{x} says {y}", args);
// Result: "10 says world"

```

All three examples compile to the same underlying mechanism: a type-erased argument store converted to a lightweight view that the formatting engine processes uniformly.

## Key Design Trade-offs

The `{fmt}` argument handling architecture makes deliberate engineering choices:

- **Contiguous storage for the hot path** — The `basic_format_arg` vector minimizes cache misses during format string parsing.
- **Separate dynamic storage for cold data** — Only values requiring allocation hit the linked list; simple types stay inline.
- **Type tags instead of RTTI** — The `detail::type` enum provides fast switching without `typeid` overhead.
- **Views instead of owning containers** — `basic_format_args` enables zero-cost passing to internal functions.

These decisions align with `{fmt}`'s goal of matching or exceeding `printf` performance while maintaining full type safety.

## Summary

- **`basic_format_arg`** in [`include/fmt/core.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h) holds type-erased values via a tagged union, removing original C++ type information at runtime.
- **`basic_format_args`** in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) provides a non-owning view over argument arrays for uniform formatter consumption.
- **`dynamic_format_arg_store`** in [`include/fmt/args.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/args.h) builds argument collections dynamically, using `need_copy<T>` to decide between referencing and heap-allocating values.
- The **copy-or-reference decision** keeps simple arguments inline while safely managing complex or temporary objects through `dynamic_arg_list`.
- The **named argument mechanism** uses index 0 placeholders and a separate name-to-index map, signaled via the `named` flag in the view.

## Frequently Asked Questions

### How does `{fmt}` avoid virtual function overhead in type erasure?

Rather than using virtual functions or `std::any`, `{fmt}` stores arguments in a union with an explicit `detail::type` enum tag. The formatter switches on this integer tag to extract the correct union member—branch prediction and inlining make this faster than virtual dispatch or RTTI lookups.

### Can I use `dynamic_format_arg_store` with custom types?

Yes. If your custom type has a `formatter<T>` specialization, `dynamic_format_arg_store` will store it via `typed_node<T>` in the `dynamic_arg_list` when `need_copy<T>` requires copying. The type erasure preserves enough information for the formatter to invoke your custom `format()` method via the stored function pointer.

### Why does `{fmt}` use a linked list for dynamic storage instead of `std::vector`?

The `dynamic_arg_list` linked list in [`args.h`](https://github.com/fmtlib/fmt/blob/main/args.h) allows heterogeneous types in a single container without `std::any`'s type-erasure overhead. Each `typed_node<T>` knows its own deallocation size, enabling type-safe cleanup without virtual destructors. This isolates allocation costs to only those arguments that require it.

### What's the performance cost of type erasure in `{fmt}`?

For primitive types and string views, the cost is negligible—the `basic_format_arg` vector stores values inline with no indirection. Heap allocation and indirect access only occur for types where `need_copy<T>` is true. Benchmarks show `{fmt}` matching or exceeding `printf` performance despite the type safety guarantees.