I want to create a pair of newtypes Tag(str)
and TagBuf(String)
,
analogous to how Path
and PathBuf
wrap OsStr
and OsString
. My
end goal is to have a map keyed by TagBuf
and to be able to index into
it with just a Tag
:
fn main() {
let mut m: HashMap<TagBuf, i32> = HashMap::new();
m.insert(TagBuf("x".to_string()), 1);
assert_eq!(m.get(Tag::new("x")), Some(&1));
}
But I’m running into issues because Tag
is dynamically sized.
Specifically, implementing Borrow<Tag> for TagBuf
is tricky:
pub struct Tag(str);
pub struct TagBuf(String);
impl std::borrow::Borrow<Tag> for TagBuf {
fn borrow(&self) -> &Tag {
let s: &str = self.0.as_str();
// How can I turn `&str` into `&Tag`? A naive attempt fails:
&Tag(*s)
}
}
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/lib.rs:8:10
|
8 | &Tag(*s)
| ^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: all function arguments must have a statically known size
I can just return unsafe { std::mem::transmute(s) }
with a
#[repr(transparent)]
annotation, but I would like to avoid unsafe
code.
I’ve looked at the source for Path
/PathBuf
and come up with the
following:
use std::borrow::Borrow;
use std::ops::Deref;
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tag(str);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct TagBuf(String);
impl Tag {
fn new<S: AsRef<str> + ?Sized>(s: &S) -> &Tag {
unsafe { &*(s.as_ref() as *const str as *const Tag) }
}
}
impl Deref for TagBuf {
type Target = Tag;
fn deref(&self) -> &Tag {
Tag::new(&self.0)
}
}
impl Borrow<Tag> for TagBuf {
fn borrow(&self) -> &Tag {
self.deref()
}
}
impl ToOwned for Tag {
type Owned = TagBuf;
fn to_owned(&self) -> TagBuf {
TagBuf(self.0.to_owned())
}
}
fn main() {
let mut m = std::collections::HashMap::<TagBuf, i32>::new();
m.insert(TagBuf("x".to_string()), 1);
assert_eq!(m.get(Tag::new("x")), Some(&1));
}
…and this works, and I can understand it (good!), but it still uses
unsafe
for that cast, which I’d like to avoid.
I saw the Rustonomicon section on exotically sized types, which
doesn’t use unsafe
, but the unsizing coercion seems complicated, and
I don’t see how to adapt it from [u8]
to str
, since there’s no
stringy counterpart to [u8; N]
.
I also read the implementation of Rc<str>
, which seems to do some more
unsafe conversion via Rc<[u8]>
and some specialization magic that
I had trouble understanding.
I’ve read some related questions, like:
- Implementing FromStr for a custom &[u8] type
- https://users.rust-lang.org/t/how-to-newtype-string/5211
…but I haven’t found an answer.
Does latest stable Rust have a way to define a newtype pair for str
and String
in safe code? If not, are there RFCs or tracking issues
that I should follow?
str
directly.&str
is the more common form, probably for this reason. – Whiteningunsafe
(or a crate that usesunsafe
) to do this. – IncaTag(str)
instead ofTag<'a>(&'a str)
. – GirasolBorrow<Tag<?>> for TagBuf
, which is required to use it as a map key? Implementingimpl<'a> Borrow<Tag<'a>> for TagBuf
doesn’t seem right: (a) that would let you get a&'static str
from a stack-ownedTagBuf
, and (b) you’d need to implementfn borrow<'a>(&'s self) -> &'s Tag<'a>
, which doesn’t look possible (for good reason). – ItuPath::new()
does use unsafe, so it might be required. – OutpourCow<'_, str>
,&(dyn Key + '_)
). While the FFI question is related and the best answers may end up being similar, the questions are sufficiently different.str
andc_void
have quite different meanings and representations (&str
is a fat pointer;&c_void
is not), and the lack of an FFI boundary simplifies things quite a lot. A canonical answer to this question would be useful to many more people than an answer to that one, so this question should stand. – Ituvoid *
is not a fat pointer is not true, actually it's whatever according to the C standard. it's an opaque type that "can allow other pointer to be cast into it and can do the reverse operation". There is some system where void * is very special. – Twilastd::mem::size_of::<&str>()
is16
on my system, whereasstd::mem::size_of::<&c_void>()
is8
. – Itunew()
implementation required to get&unsized
s intoNewtype(unsized)
, not the question itself: For anyone else who hates writing boilerplate (and unsafe boilerplate doubly so), theopaque_typedef
crate provides a derive macro to do this part for you (except it's calledfrom_inner
instead ofnew
; it also gives youas_inner
to get the&unsized
out of the newtype struct again). Might save some people some time. – Ulrica