Why doesn't Index for String delegate to Index for str?

Current nightly:

#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Index<ops::Range<usize>> for String {
    type Output = str;

    #[inline]
    fn index(&self, index: ops::Range<usize>) -> &str {
        &self[..][index]
    }
}
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Index<ops::RangeTo<usize>> for String {
    type Output = str;

    #[inline]
    fn index(&self, index: ops::RangeTo<usize>) -> &str {
        &self[..][index]
    }
}
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Index<ops::RangeFrom<usize>> for String {
    type Output = str;

    #[inline]
    fn index(&self, index: ops::RangeFrom<usize>) -> &str {
        &self[..][index]
    }
}
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Index<ops::RangeFull> for String {
    type Output = str;

    #[inline]
    fn index(&self, _index: ops::RangeFull) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}
#[stable(feature = "inclusive_range", since = "1.26.0")]
impl ops::Index<ops::RangeInclusive<usize>> for String {
    type Output = str;

    #[inline]
    fn index(&self, index: ops::RangeInclusive<usize>) -> &str {
        Index::index(&**self, index)
    }
}
#[stable(feature = "inclusive_range", since = "1.26.0")]
impl ops::Index<ops::RangeToInclusive<usize>> for String {
    type Output = str;

    #[inline]
    fn index(&self, index: ops::RangeToInclusive<usize>) -> &str {
        Index::index(&**self, index)
    }
}

#[stable(feature = "derefmut_for_string", since = "1.3.0")]
impl ops::IndexMut<ops::Range<usize>> for String {
    #[inline]
    fn index_mut(&mut self, index: ops::Range<usize>) -> &mut str {
        &mut self[..][index]
    }
}
#[stable(feature = "derefmut_for_string", since = "1.3.0")]
impl ops::IndexMut<ops::RangeTo<usize>> for String {
    #[inline]
    fn index_mut(&mut self, index: ops::RangeTo<usize>) -> &mut str {
        &mut self[..][index]
    }
}
#[stable(feature = "derefmut_for_string", since = "1.3.0")]
impl ops::IndexMut<ops::RangeFrom<usize>> for String {
    #[inline]
    fn index_mut(&mut self, index: ops::RangeFrom<usize>) -> &mut str {
        &mut self[..][index]
    }
}
#[stable(feature = "derefmut_for_string", since = "1.3.0")]
impl ops::IndexMut<ops::RangeFull> for String {
    #[inline]
    fn index_mut(&mut self, _index: ops::RangeFull) -> &mut str {
        unsafe { str::from_utf8_unchecked_mut(&mut *self.vec) }
    }
}
#[stable(feature = "inclusive_range", since = "1.26.0")]
impl ops::IndexMut<ops::RangeInclusive<usize>> for String {
    #[inline]
    fn index_mut(&mut self, index: ops::RangeInclusive<usize>) -> &mut str {
        IndexMut::index_mut(&mut **self, index)
    }
}
#[stable(feature = "inclusive_range", since = "1.26.0")]
impl ops::IndexMut<ops::RangeToInclusive<usize>> for String {
    #[inline]
    fn index_mut(&mut self, index: ops::RangeToInclusive<usize>) -> &mut str {
        IndexMut::index_mut(&mut **self, index)
    }
}

Would it not be clearer to implement it as

#[stable(feature = "rust1", since = "1.0.0")]
impl<I> Index<I> for String
where
    str: Index<I>,
{
    type Output = str;

    #[inline]
    fn index(&self, index: I) -> &str {
        &unsafe { str::from_utf8_unchecked(&self.vec) }[index]
    }
}

#[stable(feature = "derefmut_for_string", since = "1.3.0")]
impl<I> ops::IndexMut<I> for String
where
    str: IndexMut<I>,
{
    #[inline]
    fn index_mut(&mut self, index: I) -> &mut str {
        &mut unsafe { str::from_utf8_unchecked_mut(&mut *self.vec) }[index]
    }
}

? Why isn't it implemented this way? (Type inference issues?)

Historically, this dates back to when the Index trait first replaced dedicated slicing methods in the pre-1.0 days. I suspect that it survives only because nobody else has noticed or bothered with it since then.

1 Like

Ugh, I just realized why this is: it would be a breaking change to fix.

This is because it's perfectly possible for a user to implement Index<MyType> for String and Index<MyType> for str. Fixing String to use I: SliceIndex<str> like str is also likewise impossible IIUC.

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=94e7e4396addc57b9d33fd5613b564a2

This needs specialization (default impl) to fix :frowning:

Regardless of history, I think it would be a breaking change to add this now, because I can have a conflicting third-party impl like this:

struct MyRange(usize, usize);

impl std::ops::Index<MyRange> for String {
    type Output = str;
    fn index(&self, range: MyRange) -> &str {
        &self[range.0..range.1]
    }
}

impl std::ops::Index<MyRange> for str {
    type Output = str;
    fn index(&self, range: MyRange) -> &str {
        &self[range.0..range.1]
    }
}

edit: ... as you just realized yourself and sniped me. :smile:

The standard library is allowed to use specialization; String and str already use it, actually: https://github.com/rust-lang/rust/pull/32586/

1 Like

Only for non-observable aspects (not coherence violations!) mediated through an internal trait.

https://doc.rust-lang.org/nightly/src/alloc/string.rs.html#2144-2149

2 Likes

...apologies, so the suggest change would break orphan rules?

No, that it could break any code that implements Index<MyType> for String, for example, this code. Which is unfortunate, because we cannot implement a blanket impl Index<_> for String that defers to str because of this.

Note that a workaround would be to delegate through a macro, which avoids both the code repetition and coherence rules issues:

struct String {
    vec: Vec<u8>,
}

delegate_range_indexing! {
    fn index (
        self: &String,
        index: ... ,
    ) -> &str
    {
        &(unsafe {
            ::std::str::from_utf8_unchecked(&self.vec[..])
        })[index]
    }

    fn index_mut (
        self: &mut String,
        index: ... ,
    ) -> &mut str
    {
        &mut (unsafe {
            ::std::str::from_utf8_unchecked_mut(&mut self.vec[..])
        })[index]
    }
}
2 Likes