Proposal
Problem statement
"&str".to_string()
is such a fundamental part of the Rust language. It's the second function introduced by "the book" to construct String
right after the String::new()
. And still is one of the most preferable methods to construct owned String
for many experienced Rust developers. But the method is based on the Display
trait, which implies std::fmt
infrastructures with unavoidable dynamic dispatches and optimization blocker.. which is not. Since it's such a common function we've especially optimized them to ideal code with specialization.
But specialization is not a silver bullet. Since it "specialize" certain types it doesn't compose naturally. For example, (&"&str").to_string()
doesn't take specialized route and perform dynamic dispatch. Same applies for Arc<str>
, arrayvec::ArrayString
etc. and with those also-specialized primitive types like bool
, u8
but with some indirections.
Motivating examples or use cases
These .to_string()
calls invokes std::fmt
infrastructure with dynamic dispatch. Specialization optimizes few chosen cases but the condition is subtle which can be surprising.
fn cond(s: String) -> bool {...}
let b = vec!["foo", "bar"].iter().any(|s| cond(s.to_string()));
let s: Arc<str> = Arc::from("foo");
s.to_string();
// external crates defined types
let s: arrayvec::ArrayString = "foo".into();
s.to_string();
let s = FourCC::new(r"mpeg").unwrap();
s.to_string();
// and more types without touching specializations
42_i32.to_string();
std::net::Ipv4Addr::LOCALHOST.to_string()
Solution sketch
The goal of this ACP is to un-specialize impl ToString
without affecting performance, by adding a new method to the trait Display
. Currently handful of types are specialized with ToString
trait and they can be divided into 3 groups:
- Delegate to
str
-str
,String
,Cow<'_, str>
etc.- Including
bool
which is either"true"
or"false"
- Including
- Need small fixed sized buffer -
u8
,char
etc. fmt::Arguments<'_>
To address all those cases I propose to add another method to trait Display
:
trait Display {
...
fn try_to_str<'a>( // <- New!
&'a self,
fixed_buf: &'a mut [u8],
) -> Result<&'a str, TryToStrError> {
Err(TryToStrError::default()) // <- With default implementation
}
}
struct TryToStrError {...}
impl TryToStrError {
fn new() -> Self {...}
fn with_capacity_hint(self, capacity_hint: Option<usize>) -> Self {...}
fn capacity_hint(&self) -> Option<usize> {...}
}
// And replace existing specialization-based `impl ToString`s
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
const FIXED_BUF_CAPACITY: usize = 64;
let mut fixed_buf = [0_u8; FIXED_BUFFER_CAPACITY];
let mut buf = match self.try_to_str(&mut fixed_buf) {
Ok(s) => return s.to_owned(),
Err(err) => match err.capacity_hint() {
Some(capacity) => String::with_capacity(capacity),
None => String::new(),
}
};
let mut formatter = core::fmt::Formatter::new(&mut buf);
// Bypass format_args!() to avoid write_str with zero-length strs
fmt::Display::fmt(self, &mut formatter)
.expect("a Display implementation returned an error unexpectedly");
buf
}
}
// Example `impl Display` code
impl fmt::Display for str {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {...}
fn try_to_str<'a>(
&'a self,
fixed_buf: &'a mut [u8],
) -> Result<&'a str, TryToStrError> {
Ok(self)
}
}
impl fmt::Display for u8 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {...}
fn try_to_str<'a>(
&'a self,
fixed_buf: &'a mut [u8],
) -> Result<&'a str, TryToStrError> {
const REQUIRED_BUF_CAPACITY: usize = 3;
if fixed_buf.len() >= REQUIRED_BUF_CAPACITY {
// write to fixed_buf
let mut n = *self;
let mut offset = 0;
if n >= 10 {
if n >= 100 {
fixed_buf[offset] = b'0' + n / 100;
offset += 1;
n = n % 100;
}
fixed_buf[offset] = b'0' + n / 10;
offset += 1;
n = n % 10;
}
fixed_buf[offset] = b'0' + n;
offset += 1;
Ok(unsafe {
std::str::from_utf8_unchecked(&fixed_buf[..offset])
})
} else {
return Err(TryToStrError::new())
}
}
}
Buffer capacity
To make this change really zero cost it's recommended to have both FIXED_BUF_CAPACITY
(from the caller side) and REQUIRED_BUF_CAPACITY
(from the callee side) as constant, to let the branch optimized out when inlined.
The callee should know its required buffer capacity, but callers need to decide the capacity that is large enough for practical cases. For example f64
may take up to 17 bytes to print, and SocketAddr
may take 58 bytes. I suggest to use 64 as a base value since it's small enough for small stack buffer and still large enough for most leaf types.
Composite types
While the fmt
method is designed to be composited, it's not generally recommended for composite types to override try_to_str
method. This method is designed for directly consuming leaf types without any additional formatter arguments like .to_string()
. Wrapper types(like Arc<T>
) may override this method to delegate to its inner type.
Alternatives
We can keep the current specialization based structure. It's proven to be good enough for most use cases and we may still add another specialized impl when needed.
Technically this behavior can be implemented with 3rd party crate with its own ToString
and DisplayExt
traits. But its .to_string()
method can't be applied to other crate's types that only implements standard Display
trait not the 3rd party DisplayExt
trait.
Links and related work
Please help me to fill this section!