How to determine the printing width of a type that can be formatted?

Viewed 165

If a type implements Debug, how do I determine the width of what would be printed from a println!("{:?}", ...) ?

fn width<T: std::fmt::Debug>(to_print_later: &T) -> usize {
    // what goes here?
}

Preferably without actually allocating anything on the heap and definitely without actually printing anything.

2 Answers

You cannot do this without actually performing the formatting. Formatting is lazy by default and the formatting traits do not have a method that allows querying what the size would be.

Additionally, a type's implementation of Debug::fmt may or may not allocate, so it's impossible to say that no allocation will occur.

The simplest thing to do is create a string and return the length of the string:

fn width(to_print_later: &impl std::fmt::Debug) -> usize {
    format!("{:?}", to_print_later).len()
}

If you want to avoid your own allocation, you can write your own implementer of fmt::Write and format into it, only counting the length:

use std::fmt::{self, Write};

fn width(to_print_later: &impl fmt::Debug) -> usize {
    let mut c = CountingWriter::default();
    write!(&mut c, "{:?}", to_print_later).expect("Exceeded the usize");
    c.0
}

#[derive(Debug, Default)]
struct CountingWriter(usize);

impl fmt::Write for CountingWriter {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.0 = usize::checked_add(self.0, s.len()).ok_or(fmt::Error)?;
        Ok(())
    }
}

This isn't a sure performance win, as it's possible that the implementation of Debug::fmt is the heavyweight part, not the allocation. As usual, profile to know what's best for your case.


Note that I use Debug in this answer, but it applies to the other traits like Display and LowerHex as well.

You could write your own implementation of std::fmt::Write that counts instead of printing:

use std::fmt::Write;

#[derive(Default)]
struct Counter {
    count: usize,
}

impl Write for Counter {
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.count += s.len();
        Ok(())
    }

    fn write_char(&mut self, c: char) -> std::fmt::Result {
        self.count += c.len_utf8();
        Ok(())
    }
}

fn main() {
    let mut counter = Counter::default();
    write!(counter, "Number of {}: {}", "foo", 42).unwrap();

    println!("Count: {}", counter.count);
}

This won't allocate any big string, but will still allocate smaller strings for most of the parameters (because they are converted to a string before calling write_str).

Related