# Internal Architecture of fmtlib's basic_memory_buffer for Memory Allocation

> Explore fmtlib's basic_memory_buffer internal architecture. Discover how its small-size optimization avoids heap allocations for typical formatting tasks.

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

---

**The `basic_memory_buffer` implements a small-size optimization strategy that keeps the first 500 elements in inline stack storage while delegating to a custom allocator for dynamic growth, eliminating heap allocations for typical formatting operations.**

The `basic_memory_buffer` class is the backbone of string formatting operations in the **fmtlib/fmt** repository, serving as a high-performance container that balances stack efficiency with heap flexibility. Understanding its internal architecture reveals how the library minimizes memory overhead during formatting. This analysis examines the implementation in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) to explain the precise mechanisms governing memory allocation.

## Template Structure and Inline Storage

The class template `basic_memory_buffer<T, SIZE, Allocator>` inherits from `detail::buffer<T>` and implements a compile-time fixed inline buffer. The template parameter `SIZE` defaults to `inline_buffer_size` (defined at line 18 of [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) as 500 elements for `char`).

**Inline storage** resides directly in the object as `T store_[SIZE]` (line 39 in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h)). This array ensures that small formatting operations never touch the heap, providing cache-friendly locality for typical use cases. The buffer pointer in the base class initially references this stack-allocated array.

## Allocator Integration and Deallocation Strategy

Unlike standard library containers, `basic_memory_buffer` stores its allocator as a direct member `alloc_` (line 42), defaulting to `detail::allocator<T>`. The implementation utilizes the `FMT_NO_UNIQUE_ADDRESS` attribute to prevent unnecessary object padding when using stateless allocators.

The `deallocate()` method (line 44) handles cleanup by checking `data() != store_` before invoking `alloc_.deallocate`. This pointer comparison distinguishes between inline stack memory and dynamically allocated heap blocks, ensuring the destructor only releases resources that were actually allocated from the free store.

## Dynamic Growth and Capacity Management

When formatting output exceeds the inline capacity, the static `grow()` function (line 50) orchestrates the expansion. This method implements a **geometric growth policy** with the following steps:

- **Capacity calculation**: Computes new capacity as approximately 1.5 times the current size (old capacity plus half the old capacity)
- **Safety validation**: Ensures the new capacity does not exceed `alloc_.max_size()` to prevent overflow
- **Allocation**: Acquires new storage via `alloc_.allocate`
- **Data migration**: Transfers existing elements using `memcpy` for trivially copyable types
- **Pointer update**: Redirects the base class buffer pointer to the new heap block
- **Cleanup**: Releases the previous heap allocation if it was not the inline store

This strategy amortizes allocation costs while maintaining the `detail::buffer<T>` interface contract used throughout the formatting pipeline.

## Move Semantics and Allocator Propagation

The buffer implements allocator-aware move semantics through two overloads of `move_alloc` (line 86). These helpers determine whether the allocator supports propagation on move operations—if so, the allocator is transferred; otherwise, data must be copied to respect allocator identity.

The `move()` method (line 108) performs the actual transfer logic:
- If the source buffer uses inline storage (`store_`), it copies the data elements
- If the source owns a heap allocation, it transfers pointer ownership and resets the source to its inline buffer

The move constructor (line 127) delegates to `move()`, while the move-assignment operator (line 134) first deallocates any existing heap memory via `deallocate()` before transferring ownership. This ensures exception-safe resource management during container relocation.

## Construction and Initialization

The explicit constructor (line 78) initializes the base `detail::buffer<T>` with the `grow` function pointer and sets the initial buffer reference to `store_`. During constant evaluation contexts, the constructor also zero-fills the inline storage to guarantee deterministic behavior for `constexpr` formatting operations.

## Practical Usage Example

The following example demonstrates how the buffer behaves under different size constraints:

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

using fmt::memory_buffer;  // Alias for basic_memory_buffer<char>

int main() {
    // Small string stays in inline storage (no heap allocation)
    memory_buffer buf1;
    fmt::format_to(buf1, "Hello {}", "world");
    fmt::print("{}\n", fmt::to_string(buf1));  // Output: Hello world

    // Large string triggers heap allocation after exceeding 500 chars
    memory_buffer buf2;
    fmt::format_to(buf2, "{}", std::string(2000, 'x'));
    fmt::print("Size after large write: {}\n", buf2.size());

    // Move transfers heap ownership without copying
    memory_buffer buf3 = std::move(buf2);
    fmt::print("Moved size: {}\n", buf3.size());
}

```

In this example:
- `buf1` remains within the `inline_buffer_size` limit, keeping all data on the stack
- `buf2` exceeds the threshold, triggering `grow()` and allocating a heap block
- Moving `buf2` into `buf3` transfers pointer ownership without reallocating, courtesy of the `move()` implementation

## Summary

- **Hybrid storage model**: `store_[SIZE]` provides inline stack storage for 500 elements by default, while the `grow()` function handles transitions to heap allocation
- **Geometric expansion**: The buffer increases capacity by approximately 1.5x when growing, respecting the allocator's `max_size()` constraints
- **Safe deallocation**: The `deallocate()` method uses pointer comparison (`data() != store_`) to distinguish inline from heap memory
- **Allocator propagation**: Move operations utilize `move_alloc` helpers to correctly handle stateful allocators per standard propagation traits
- **Zero-overhead abstraction**: The implementation in [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) provides dynamic container semantics with static buffer performance for common cases

## Frequently Asked Questions

### What is the default inline buffer size in fmtlib's basic_memory_buffer?

The default inline buffer size is defined by the `inline_buffer_size` constant (line 18 of [`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h)), which provides space for 500 `char` elements. This allows typical formatting operations to complete entirely on the stack without invoking the heap allocator.

### How does basic_memory_buffer decide when to allocate from the heap?

The buffer tracks its current size against capacity through the base `detail::buffer<T>` class. When an operation would exceed capacity, the static `grow()` function (line 50) allocates a new heap block via `alloc_.allocate`, migrates existing data using `memcpy`, and updates the internal pointer. The first allocation occurs only when the required capacity exceeds the compile-time `SIZE` template parameter.

### Is basic_memory_buffer allocator-aware?

Yes, the class stores an `Allocator` member (`alloc_`) using the `FMT_NO_UNIQUE_ADDRESS` attribute to optimize layout. It respects allocator propagation traits during move operations through the `move_alloc` overloads (line 86), and checks `alloc_.max_size()` during growth to prevent overflow. This design supports both stateless and stateful custom allocators.

### What growth factor does fmtlib use for buffer expansion?

The implementation uses a geometric growth factor of approximately 1.5x, calculated as `old_capacity + old_capacity / 2` in the `grow()` function. This policy balances memory efficiency against reallocation frequency, minimizing the number of expensive allocation operations as the buffer scales to arbitrary sizes.