Skip to content

Instantly share code, notes, and snippets.

@tigregalis
Last active July 18, 2025 02:14
Show Gist options
  • Save tigregalis/e8e50764f9d8580f357833af76a91b78 to your computer and use it in GitHub Desktop.
Save tigregalis/e8e50764f9d8580f357833af76a91b78 to your computer and use it in GitHub Desktop.
DelimitedStringBuilder
use std::fmt::Write;
use std::fmt::Arguments;
/// This type simulates pushing into a `Vec<String>` and similar,
/// and then joining with a delimiter,
/// but it's optimised for minimising allocations.
/// It does zero allocations of its own.
///
/// TODO: Instead of String use a Writer,
/// then you could write directly to a stream.
pub struct DelimitedStringBuilder<'string, P: AsRef<str>> {
pattern: P,
string: &'string mut String,
first: bool,
}
impl<'string, P: AsRef<str>> DelimitedStringBuilder<'string, P> {
pub fn new(string: &'string mut String, pattern: P) -> Self {
string.clear();
Self {
pattern,
string,
first: true,
}
}
/// Push a string.
///
/// NOTE: Instead of `sb.push_str(format!("{foo}"))`
/// use `sb.push_fmt(format_args!("{foo}"))`.
/// See [`push_fmt`];
pub fn push_str(&mut self, s: impl AsRef<str>) {
if !self.first {
self.string.push_str(self.pattern.as_ref());
} else {
// Hint to the compiler that this branch is unlikely
cold();
self.first = false;
}
self.string.push_str(s.as_ref());
}
/// Push a formatted string ([`Arguments`]),
/// which avoids allocation of the [`format`] macro.
///
/// We unfortunately can't just implement `Write` and use `write!` for this type
/// because it is semantically incorrect:
/// `write!` will call `write_fmt` for each argument,
/// resulting in unintentional delimiters being written as well,
/// since there's no way to observe that we are in the middle of writing a single element.
///
/// ```
/// let mut s = String::new();
/// let mut sb = DelimitedStringBuilder::new(&mut s, " ");
/// let name = "Alice";
/// sb.push_fmt(format_args!("{name}"));
/// ```
pub fn push_fmt(&mut self, args: Arguments<'_>) {
if !self.first {
self.string.push_str(self.pattern.as_ref());
} else {
// Hint to the compiler that this branch is unlikely
cold();
self.first = false;
}
self.string.write_fmt(args).ok();
}
/// "Clear" the builder. Equivalent to calling `Vec::clear()`.
pub fn clear(&mut self) {
self.string.clear();
self.first = true;
}
pub fn get(&self) -> &str {
&self.string
}
pub fn is_empty(&self) -> bool {
self.first
}
// TODO: implement other Vec- / collection- / Iterator-like methods
}
/// Hint to the compiler that this branch is unlikely
#[inline(always)]
#[cold]
fn cold() {}
fn main() {
let mut buf = String::new();
let mut sb = DelimitedStringBuilder::new(&mut buf, " ".to_owned());
sb.push_str("Hello,");
sb.push_str("world!");
println!("{}", sb.get());
sb.clear();
sb.push_str("It's");
sb.push_str("time");
sb.push_str("for");
let exclamations = "!".repeat(rand::random_range(1..=10));
sb.push_fmt(format_args!("revolution{exclamations}"));
println!("{}", sb.get());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment