Mut String += format_args!(…);

I have various macros that boil down to format_args!(). They go into one another. Sometimes I also want to append them to an existing String. If I used format!() or .to_string(), it would work, at the price of an unwanted heap allocation.

Instead, what I'd like is the following. Of course, owning neither AddAssign nor String I can't currently do this myself. (For now I have an ugly workaround by wrapping the String.) I think this would make sence as part of std. Not sure about the Err. I hope this concrete usage couldn't cause more trouble than the existing += str.

impl std::ops::AddAssign<std::fmt::Arguments<'_>> for String {
    fn add_assign(&mut self, other: std::fmt::Arguments<'_>) {
        std::fmt::write(self, other).unwrap();
    }
}
1 Like

I'm having a hard time imagining your exact use-case from this vagus description alone. Would you mind elaborating a bit what "boil down to format_args!()" means, and how you use your macros so that you "sometimes append to a String" (and presumably sometimes do something else)? Like, some code example of what the call to the macro would like and how it's improved with such a += implementation. Maybe, also the question "why isn't fmt::write and/or write! sufficient" should be answered.

2 Likes

The problem is that the efficient way is more verbose than less performant ways of appending formatted string:

use std::fmt::Write;
let _ = write!(&mut s, "{x}");

vs

s += &format!("{x}");

or

s.push_str(&format!("{x}"));

While I usually don't like the + overload on strings, I think support for format_args! here is a very clever trick, and gives String a pretty compact and relatively efficient formatted append.

17 Likes

One thing that could help this is if there were an inherent write_fmt(…) -> () on String, then you wouldn’t need to use the trait or discard the return value.

1 Like

That would be very nice, but it would break existing code that calls write!(string, "...").unwrap().

Mmmm, unless it was done as an edition hack like array intoiterator, and that doesn’t feel worthwhile to me.

Relevant ACP to make write!(str, ...) a little easier.

And as pointed out there, rust-analyzer has this macro:

 /// Appends formatted string to a `String`. 
 #[macro_export] 
 macro_rules! format_to { 
     ($buf:expr) => (); 
     ($buf:expr, $lit:literal $($arg:tt)*) => { 
         { use ::std::fmt::Write as _; let _ = ::std::write!($buf, $lit $($arg)*); } 
     }; 
 }
2 Likes

At that point, why bother with write! instead of <_ as ::std::fmt::Write>::write_fmt(...)?

By boil down I mean, they are construction helpers for things like urls and html constructs and tags. Each ultimately returns a format_args!().

Instead of needing to explicitly do fmt::write stuff, I'm looking for nice syntax:

let mut s = "prefix".to_string();
s += html_popup!(… tag!(… url!(…) …) …);
// which macro-expands to
s += format_args!(…);

This seems sensible as format_args!() is semantically stringish.

Thanks for the clarification. That's what I think I'd have guessed, too, but I like the more explicit code example to be sure there's no misinterpretation :innocent:

write! contains the format_args! within itself, the two options would be like

write!(&mut s, "Hello {name}!");
s.write_fmt(format_args!("Hello {name}!"));

Eh, ok. The embedded use annoys me, but I suppose it's scoped. They may also want the auto-ref method resolution of .write_fmt vs. ::write_fmt.

Would it be possible to have an attribute to suppress the #[must_use] of a type when defining a function with said function as return type? Then String::write_fmt could use this attribute to indicate that it never returns an error.

1 Like

Speaking of which, it would be nice to have a try-variant that did report errors on failed allocations etc.

2 Likes

Given pattern types, we could specify that a function never returns Err.

1 Like

Is there perhaps desire for a (name to be determined, …, let’s call it push_display for now) method on String with a

fn push_display(&mut self, x: impl fmt::Display) -> ()

or alternatively

fn push_display(&mut self, x: &impl fmt::Display) -> ()

signature? I.e. supporting not only fmt::Arguments<'_> but simply any Display type.


Probably implemented along the lines of

{
    x.fmt(&mut Formatter::new(self)).unwrap()
}

which should be equivalent (but slightly cheaper) compared to

{
    self.write_fmt(format_args!("{x}")).unwrap()
}

(it’s a bit like a slight generalization[1] of the ToString (default) implementation; also peeking there, looks like the .unwrap() should instead be “.expect("a Display implementation returned an error unexpectedly")”; and we might also want to mirror, or perhaps even somehow combine, the specialization of ToString to get the same performance; e.g. for &str it should simply call push_str.)


  1. as in: it doesn’t also create the String ↩︎

7 Likes

I do like s += format_args!("...");, that seems indeed more discoverable than write!(s, "...").unwrap(). So it seems better to me than what my ACP proposes.

s.push_display(format_args!("...))" also works I guess but that's more nested so a bit harder to type and read.

5 Likes

One issue with += supporting fmt::Arguments<'_> is that it would break code like

fn main() {
    let mut s = String::from("Hello");
    let additional = String::from(" World!");
    s += &additional;
}

As a demo see how the use-case for this re-implementation of the relevent API breaks by the additional impl.

error[E0277]: cannot add-assign `&String` to `String`
  --> src/main.rs:27:7
   |
27 |     s += &additional;
   |       ^^ no implementation for `String += &String`
   |
   = help: the trait `AddAssign<&String>` is not implemented for `String`
   = help: the following other types implement trait `AddAssign<Rhs>`:
             <String as AddAssign<Arguments<'_>>>
             <String as AddAssign<&str>>

Maybe, common use-cases can be saved by also adding extra implementations for RHS types such as &String, but there’d always be at least some degree of breakage left.

Although now, on second thought, I’m kind-of wondering, are there any common crates that come with a AddAssign<CustomType> for String implementation, and if so would helping users of such crates motivate additional implementations for things like &String anyways.

On third thought… @daniel-pfeiffer instead of wrapping the string, you can also wrap the format_args into a new type that implements both Display and AddAssign<Self> for String; maybe that’s the nicer quick workaround?

5 Likes

Yet another case where deref coercion during trait resolution would help. Side-note: I haven't tried making that into a full-blown RFC yet because it needs to support more than just deref coercion for things like arrays.

1 Like

Another less ideal aspect to += is that it raises the question of whether we should support +. If it’s not supported, it makes the String API a bit weird. But if we do support it, what types should/could work with Add?