What is the difference between itoa::FmtWriteTarget and String as a destination for integer formatting?

FmtWriteTarget enables zero-allocation integer formatting by writing to any fmt::Write target, while String always allocates new storage for the formatted output. The itoa crate provides fast integer-to-string conversion, and FmtWriteTarget is the mechanism that lets you reuse existing buffers instead of creating new allocations for each conversion. This matters in hot loops where allocation overhead from repeated format! or .to_string() calls becomes significant.

Basic Integer Formatting with itoa

use itoa::Integer;
 
fn basic_formatting() {
    let value: i32 = 42;
    
    // Method 1: Format to String (allocates)
    let string: String = itoa::fmt_buffered(value);
    
    // Method 2: Format into existing buffer
    let mut buffer = itoa::Buffer::new();
    let string: &str = buffer.format(value);
    
    // The buffer approach is more efficient for repeated use
    // because the buffer can be reused
}

The itoa crate provides multiple ways to format integers, with different allocation characteristics.

String vs FmtWriteTarget

use itoa::{Integer, FmtWriteTarget};
use std::fmt::Write;
 
fn string_allocation() {
    let value: i32 = 42;
    
    // Using String - each call allocates
    let string1: String = format!("{}", value);  // Allocation
    let string2: String = format!("{}", value);  // Another allocation
    
    // Even with itoa, fmt_buffered allocates
    let string3: String = itoa::fmt_buffered(value);  // Allocation
    
    // Problem: In a loop, this adds up
    let mut results = Vec::new();
    for i in 0..1000 {
        results.push(format!("{}", i));  // 1000 allocations!
    }
}
 
fn reuse_with_fmt_write_target() {
    let mut output = String::with_capacity(64);
    let target = FmtWriteTarget::new(&mut output);
    
    // Format into the same String without new allocations
    for i in 0..10 {
        output.clear();  // Reuse the allocation
        write!(&mut target, "{}", i).unwrap();
        // Now output contains the formatted number
        println!("{}", output);
    }
    
    // Only one allocation (the initial String)
    // All formatting writes into existing capacity
}

FmtWriteTarget wraps a fmt::Write target and allows reuse of the underlying buffer.

FmtWriteTarget Mechanism

use itoa::FmtWriteTarget;
use std::fmt::Write;
 
fn fmt_write_target_basics() {
    // Create a destination buffer
    let mut buffer = String::new();
    
    // Create a FmtWriteTarget wrapper
    let mut target = FmtWriteTarget::new(&mut buffer);
    
    // Write integers to it
    write!(&mut target, "{}", 42).unwrap();
    assert_eq!(&buffer, "42");
    
    // Clear and reuse
    buffer.clear();
    write!(&mut target, "{}", 12345).unwrap();
    assert_eq!(&buffer, "12345");
    
    // The key insight: buffer is reused, not reallocated
    // (assuming capacity is sufficient)
}
 
fn building_strings_incrementally() {
    let mut output = String::with_capacity(256);
    let target = FmtWriteTarget::new(&mut output);
    
    // Build a string with multiple numbers
    write!(&mut target, "First: {}").unwrap();
    write!(&mut target, "Second: {}").unwrap();
    
    // Compare to format! which allocates for each operation
    let inefficient = format!("First: {}", 1) + &format!("Second: {}", 2);
    // Two allocations vs one (with FmtWriteTarget)
}

FmtWriteTarget implements fmt::Write, making it usable with write! macros.

Using itoa::Buffer Directly

use itoa::Buffer;
 
fn buffer_approach() {
    // The most common itoa pattern: Buffer.format()
    let mut buffer = Buffer::new();
    
    // format() returns a &str into the buffer
    let s: &str = buffer.format(42);
    assert_eq!(s, "42");
    
    // Reuse the same buffer
    let s: &str = buffer.format(12345);
    assert_eq!(s, "12345");
    
    // Key difference: format() returns &str, not String
    // No allocation on the heap per call
}
 
fn buffer_vs_string() {
    let mut buffer = Buffer::new();
    
    // Buffer.format - no allocation
    let s1: &str = buffer.format(42);
    let s2: &str = buffer.format(43);
    // s1 and s2 point into the same internal buffer
    // They're only valid until the next format call
    
    // Compare to String::from - always allocates
    let s3: String = 42.to_string();  // Allocates
    let s4: String = 43.to_string();  // Allocates
}

The Buffer type is the most direct way to use itoa for zero-allocation formatting.

When to Use Each Approach

use itoa::{Buffer, FmtWriteTarget, Integer};
use std::fmt::Write;
 
fn hot_loop_formatting() {
    // Scenario: Formatting millions of integers in a loop
    
    // APPROACH 1: to_string() - BAD for hot loops
    fn inefficient(values: &[i32]) -> Vec<String> {
        values.iter().map(|&v| v.to_string()).collect()
        // Each .to_string() allocates a new String
        // For 1 million values: 1 million allocations
    }
    
    // APPROACH 2: Reused Buffer - GOOD for hot loops
    fn efficient(values: &[i32]) -> Vec<String> {
        let mut buffer = Buffer::new();
        values.iter()
            .map(|&v| buffer.format(v).to_owned())
            .collect()
        // Still creates Strings for results
        // But formatting doesn't allocate
    }
    
    // APPROACH 3: FmtWriteTarget for string building
    fn build_output(values: &[i32]) -> String {
        let mut output = String::with_capacity(values.len() * 11);  // Estimate capacity
        let target = FmtWriteTarget::new(&mut output);
        
        for (i, &v) in values.iter().enumerate() {
            if i > 0 {
                output.push(',');
            }
            write!(&mut target, "{}", v).unwrap();
        }
        output
        // Single allocation for entire output
    }
}

Choose based on allocation patterns and output requirements.

Performance Characteristics

use itoa::{Buffer, FmtWriteTarget};
use std::fmt::Write;
 
fn performance_comparison() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Method           β”‚ Allocations β”‚ Use Case                      β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ format!("{}", n) β”‚ 1 per call  β”‚ One-off formatting           β”‚
    // β”‚ n.to_string()    β”‚ 1 per call  β”‚ Converting to owned String    β”‚
    // β”‚ Buffer::format   β”‚ 0 per call  β”‚ Hot loops, temporary strings β”‚
    // β”‚ FmtWriteTarget   β”‚ 0 per call  β”‚ Building strings incrementallyβ”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    let mut buffer = Buffer::new();
    
    // Zero-allocation formatting
    for i in 0..1_000_000 {
        let s: &str = buffer.format(i);
        // Use s immediately (don't store it)
        // No allocation happened
    }
    
    // With FmtWriteTarget for building output
    let mut output = String::new();
    let target = FmtWriteTarget::new(&mut output);
    
    for i in 0..100 {
        write!(&mut target, "{}", i).unwrap();
    }
    // One allocation total (when output grows)
}
 
fn allocation_overhead() {
    // Each String allocation has overhead:
    // - Memory allocation syscall
    // - String metadata (pointer, length, capacity)
    // - Potential reallocation if capacity exceeded
    
    // For 1 million integers formatted:
    // - to_string(): ~1 million allocations
    // - Buffer.format(): 1 allocation (the buffer itself)
    
    // FmtWriteTarget advantage:
    // - Write to any fmt::Write destination
    // - Can write directly to files, network, etc.
}

Buffer and FmtWriteTarget eliminate per-call allocation overhead.

FmtWriteTarget with Different Destinations

use itoa::FmtWriteTarget;
use std::fmt::Write;
 
fn various_destinations() {
    // Destination 1: String
    let mut string = String::new();
    let mut target = FmtWriteTarget::new(&mut string);
    write!(&mut target, "{}", 42).unwrap();
    assert_eq!(&string, "42");
    
    // Destination 2: Building complex output
    let mut output = String::with_capacity(100);
    let target = FmtWriteTarget::new(&mut output);
    
    write!(&mut target, "Count: {}", 5).unwrap();
    write!(&mut target, ", Total: {}", 500).unwrap();
    assert_eq!(&output, "Count: 5, Total: 500");
    
    // Destination 3: Works with any fmt::Write
    // Including custom implementations
}
 
fn custom_write_target() {
    use std::fmt;
    
    // Custom type implementing fmt::Write
    struct LogWriter(Vec<u8>);
    
    impl fmt::Write for LogWriter {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            self.0.extend(s.bytes());
            Ok(())
        }
    }
    
    let mut writer = LogWriter(Vec::new());
    let target = FmtWriteTarget::new(&mut writer);
    
    write!(&mut target, "Value: {}", 42).unwrap();
    assert_eq!(&writer.0, b"Value: 42");
}

FmtWriteTarget works with any type implementing fmt::Write.

Integration Patterns

use itoa::{Buffer, Integer};
 
fn serialize_sequence(values: &[i32]) -> String {
    let mut buffer = Buffer::new();
    let mut output = String::with_capacity(values.len() * 11);
    
    output.push('[');
    for (i, &v) in values.iter().enumerate() {
        if i > 0 {
            output.push_str(", ");
        }
        output.push_str(buffer.format(v));
    }
    output.push(']');
    output
}
 
fn csv_formatting(rows: &[Vec<i32>]) -> String {
    let mut buffer = Buffer::new();
    let mut output = String::new();
    
    for (row_idx, row) in rows.iter().enumerate() {
        if row_idx > 0 {
            output.push('\n');
        }
        for (col_idx, &v) in row.iter().enumerate() {
            if col_idx > 0 {
                output.push(',');
            }
            output.push_str(buffer.format(v));
        }
    }
    output
}
 
fn main_example() {
    let data = vec![
        vec![1, 2, 3],
        vec![10, 20, 30],
        vec![100, 200, 300],
    ];
    
    let csv = csv_formatting(&data);
    assert_eq!(csv, "1,2,3\n10,20,30\n100,200,300");
}

Using Buffer for the hot path while building a larger String output is a common pattern.

Practical Example: JSON-like Formatting

use itoa::{Buffer, FmtWriteTarget, Integer};
use std::fmt::Write;
 
fn format_object(fields: &[(&str, i32)]) -> String {
    let mut output = String::with_capacity(256);
    let target = FmtWriteTarget::new(&mut output);
    
    output.push('{');
    for (i, (key, value)) in fields.iter().enumerate() {
        if i > 0 {
            output.push_str(", ");
        }
        write!(&mut target, "\"{}\": {}", key, value).unwrap();
    }
    output.push('}');
    output
}
 
fn format_array(values: &[i32]) -> String {
    let mut buffer = Buffer::new();
    let mut output = String::with_capacity(values.len() * 12);
    
    output.push('[');
    for (i, &v) in values.iter().enumerate() {
        if i > 0 {
            output.push_str(", ");
        }
        output.push_str(buffer.format(v));
    }
    output.push(']');
    output
}
 
fn json_like_example() {
    let obj = format_object(&[
        ("id", 42),
        ("count", 100),
        ("total", 9999),
    ]);
    assert_eq!(obj, "{\"id\": 42, \"count\": 100, \"total\": 9999}");
    
    let arr = format_array(&[1, 2, 3, 4, 5]);
    assert_eq!(arr, "[1, 2, 3, 4, 5]");
}

This pattern avoids allocations for each number while building structured output.

Comparison Summary

use itoa::{Buffer, FmtWriteTarget, Integer};
use std::fmt::Write;
 
fn summary() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Approach          β”‚ Ownership      β”‚ Allocation β”‚ Use Case         β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ String::to_string β”‚ Owned String   β”‚ 1 per call β”‚ One-off use      β”‚
    // β”‚ Buffer::format    β”‚ Borrowed &str  β”‚ 0 per call β”‚ Hot loops        β”‚
    // β”‚ FmtWriteTarget    β”‚ Writes to dest β”‚ 0 per call β”‚ Building strings β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // Buffer.format() returns &str with limited lifetime
    let mut buffer = Buffer::new();
    let s: &str = buffer.format(42);
    // s is valid until next buffer.format() call
    
    // FmtWriteTarget writes to your destination
    let mut output = String::new();
    let target = FmtWriteTarget::new(&mut output);
    write!(&mut target, "{}", 42).unwrap();
    // output now contains "42"
    
    // String approach - simple but allocates
    let s: String = 42.to_string();
    // Always allocates a new String
    
    // Key insight: Buffer is usually preferred for hot paths
    // FmtWriteTarget is for when you need fmt::Write integration
}

Choose based on whether you need ownership, lifetime management, or fmt::Write integration.

Synthesis

use itoa::{Buffer, FmtWriteTarget, Integer};
use std::fmt::Write;
 
fn complete_guide_summary() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ FmtWriteTarget vs String                                                β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ String:                                                                 β”‚
    // β”‚   - Allocates on every format operation                                β”‚
    // β”‚   - Returns owned String                                               β”‚
    // β”‚   - Simple API, familiar to all Rust users                            β”‚
    // β”‚   - Use for: one-off conversions, owned output needed                 β”‚
    // β”‚                                                                         β”‚
    // β”‚ FmtWriteTarget:                                                         β”‚
    // β”‚   - Writes to existing fmt::Write destination                          β”‚
    // β”‚   - Zero per-call allocation (reuses destination's buffer)            β”‚
    // β”‚   - Requires managing the destination buffer                            β”‚
    // β”‚   - Use for: hot loops, building strings incrementally                 β”‚
    // β”‚                                                                         β”‚
    // β”‚ Buffer::format (alternative):                                           β”‚
    // β”‚   - Returns &str into internal buffer                                  β”‚
    // β”‚   - Zero per-call allocation                                           β”‚
    // β”‚   - Lifetime tied to Buffer (next call invalidates)                    β”‚
    // β”‚   - Use for: hot loops where string is immediately consumed             β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // Practical recommendations:
    
    // 1. Single conversion, need owned String:
    let s = 42.to_string();  // or format!("{}", 42)
    
    // 2. Hot loop, immediate use:
    let mut buffer = Buffer::new();
    for i in 0..1_000_000 {
        let s = buffer.format(i);
        // Use s immediately, don't store
    }
    
    // 3. Building string from multiple values:
    let mut output = String::with_capacity(1024);
    let target = FmtWriteTarget::new(&mut output);
    for i in 0..100 {
        if i > 0 { output.push(','); }
        write!(&mut target, "{}", i).unwrap();
    }
    // output: "0,1,2,3,...,99"
}
 
// Key insight:
// FmtWriteTarget is the itoa way to write integers to any fmt::Write target.
// It enables zero-allocation integer formatting when you already have a
// buffer (String, Vec<u8>, file, etc.) that can receive the formatted output.
// String::to_string() always allocates because it creates a new String each time.
// For high-performance scenarios with repeated formatting, FmtWriteTarget
// (or Buffer::format) eliminates the per-call allocation overhead.

Key insight: FmtWriteTarget bridges itoa's fast integer formatting with fmt::Write destinations, letting you format integers into existing buffers without allocation. While String::to_string() is convenient, it allocates on every callβ€”a non-issue for occasional formatting but a performance killer in tight loops. The Buffer::format() approach returns a &str reference into an internal buffer, perfect when you consume the result immediately. FmtWriteTarget extends this by working with any fmt::Write destination, making it ideal for incrementally building strings with embedded numbers where you want to avoid the allocation storm of repeated format! or .to_string() calls.