Need explanation for passing ?Sized as value in generic declaration

Just read some documentation for vector,

pub trait Index<Idx> 
where
    Idx: ?Sized, 
{
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

(06/14/20: to address any confusion, only unsized case is discussed here. sized case is a no brainer)

parameter index can be unsized local.

  1. I wonder if rust internally uses unsized local a lot? Unsized local is widely used in the doc but it's not allowed for the user. So it's hard to read the doc because this contradicts to what we are told to use (trait object or slice).

source

impl<T, I: SliceIndex<[T]>> Index<I> for Vec<T> {
    type Output = I::Output;

    #[inline]
    fn index(&self, index: I) -> &Self::Output {
        Index::index(&**self, index)
    }
}

Is using default index implementation:

impl<T, I> ops::Index<I> for [T]
where
    I: SliceIndex<[T]>,
{
    type Output = I::Output;

    #[inline]
    fn index(&self, index: I) -> &I::Output {
        index.index(self)
    }
}
  1. probably a dumb question. In the above code,
    fn index(&self, index: I) -> &Self::Output {
        Index::index(&**self, index)
    }

Index::index looks quite weird. This seems to use function overloading. Is it equivalent to index.index(&**self)? Do Rustaceans have any preference or recommendations?

playground

struct A {}

trait Foo {
    fn method_by_ref(&self);
}


impl Foo for A {
    fn method_by_ref(&self) { println!("a"); }
}

pub fn main() {
    let a = A{};
    Foo::method_by_ref(&a);
    // is equivalent to
    a.method_by_ref();
}

Is there any official name for the fully qualified form (the first one)?

For the second question:

It would be equivalent to (&**self).index(index) - receiver is the first argument, not the second.

Trait implementations of std::sync::Mutex seem to have similar trait bounds.
For example, Mutex has impl<T: ?Sized + Default> Default for Mutex<T> but obviously Default::default() cannot return unsized values (at least for now).

update (06/14/20): you are right! I jump one step further. Thanks for the reply from

(&**self).index(index) will later trigger below:

impl<T, I> ops::Index<I> for [T]
where
    I: SliceIndex<[T]>,
{
    type Output = I::Output;

    #[inline]
    fn index(&self, index: I) -> &I::Output {
        index.index(self)
    }
}

And then trigger:

source for SliceIndex[T] for usize if you use index with usize value, e.g., vec[8] .

    #[inline]
    fn index(self, slice: &[T]) -> &T {
        // N.B., use intrinsic indexing
        &(*slice)[self]
    }

The index parameter in

pub trait Index<Idx> 
where
    Idx: ?Sized, 
{
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

is not an unsized local. It is a generic parameter with no restrictions on what types can be used in its place, so we might have Idx = usize, which would certainly not be an unsized local. Sure, the where-bounds don't disallow unsized types, but you will get another compiler error if you try.

It is likely done because if something is not allowed for one reason, there's no reason to use several methods at the same time to disallow it.

6 Likes

This is not really function overloading but rather just generics + traits. One could think of Index::index as coming from a definition like this:

mod Index {
    fn index<I, T: Index<I>>(this: &T, ix: I) -> &T::Output { ... }
}

except for that T and I cannot actually be given as type parameters to Index::index like Index::index::<I,T>(...) but you would have to write <T as Index<I>>::index(...), and another difference is that you cannot do use Index::index. (Something that bugs me even more about Default::default(). You could however define a wrapper like

fn index<I, T: Index<I> + ?Sized>(this: &T, ix: I) -> &T::Output {
    Index::index(this, ix)
}

to have an index function without a path. I’m just noticing that this does restrict I to be sized now...


...not restricting it would probably look like this (using nightly):

#![feature(unsized_locals)]
use core::ops::Index;

fn index<I: ?Sized, T: Index<I> + ?Sized>(this: &T, ix: I) -> &T::Output {
    Index::index(this, ix)
}

Which does compile.

Which naturally leads me to answering your main question (something I didn’t even plan to do): Like @alice already explained, the way that the trait Index is defined doesn’t need unsized locals (you can easily define the same kind of trait yourself). But as you can see here, it doesn’t stand in the way of unsized locals either. Toy example of using an Index impl with unsized locals.

1 Like

The formal term for this is "Universal Function Call Syntax" or "Disambiguating Function Calls", and The Reference explains how it works, plus provides a link to the RFC that introduced it.

You don't normally see it too often because people naturally try to avoid naming collisions. However, it's useful when you need to unambiguously say you want to invoke that method when writing code which might be used with arbitrary other traits and types in scope (mostly macros).

1 Like

Makes sense! Thanks!

Kind of understand what this reply says: it's forward compatible with unsized local.

I just wonder, it's quite confusing since stable build doesn't allow unsized type to be passed by value, should I interpret this behavior when reading stable rust as ignoring unsized part when T: ?Sized being passed as value instead of reference?

The trait itself allows unsized values, but when you actually try to implement it without #![feature(unsized_locals)] the compiler will give an error. Stable rust doesn't ignore the unsized part of T: ?Sized. For example struct Box<T: ?Sized>(*mut T); does allow Box<[u8]> on stable despite T being unsized. It is just when the unsized T is used without being behind a reference that the compiler says no.

1 Like

Yeah, that' what I'm saying. When passing T: ?Sized as value, ignore the unsized part and T must be sized for current rust with out unsized local enabled (to be more precise).

Or understand it as T can be any type and compiler will restrict it depending on compiler settings or flags.

[Written before the previous responder finished, so I’m repeating stuff]
I think you need to differentiate between trait definition and implementation. When the trait is defined and a method declaration looks like it might be getting an T: ?Sized as an argument, the compiler doesn’t care. If you however try to provide a corresponding trait implementation where your actual code tries to handle a generic T: ?Sized as an argument, then it will complain.

I made up a dialogue about the situation.

Coder: “I’m defining a protocol (trait), someone who follows it will generate money.”
Compiler: “Sure, I’ll remember this protocol.”
Coder: “I’m registering this money-generating procedure so I need you to check that it’s legal and also check that it follows the money-generating protocol I defined. Here’s how it goes: I’ll just generate money (by which I mean, I’ll just print the bank notes myself).”
Compiler: “Wait, that’s illegal!”

3 Likes

Well, the thing is, it's a nightly-only feature. So it's not allowed by the stable compiler, but it has to be documented somewhere (otherwise how would users of nightly Rust know what to look for and where?)

TL;DR I don't think this is particularly weird.

Thanks! Oh well, the learning curve of Rust is not that low compared with C++. It took almost all useful and commonly used features from C++. If learning curve for C++ is 10, then rust will be 8...

Agreed. That will address a lot of confusion. But it will shock people (IMO) if trait object concept deeply carves in their mind already.

Not sure what you mean by that? Trait objects are a completely different thing (and they are usable on stable, too).

Nice dialog! Thanks a lot! Good clarification on declaration and definition for generic. That answers my misunderstanding too that the compiler is smart enough to check illegal usages on the declaration for generic. The check for generic only happens during monomorphization (at least for now).

For any non-generic declaration, it will error out at declaration time. See playground.

Not true. The error is something else here, and can be fixed.

To clarify. you should use trait type by relying on trait object (sized) indirectly, instead of passing unsized trait typed value around without delegating to the fat pointers.

With that being said, you need to use a reference (fat pointer) for trait. But for unsized local, you don't have to obey that rule. Hope I make my words clear.

you are right. My example is wrong since Foo cannot be made to a trait object. Makes sense. It doesn't error out until definition time.

Is this considered as definition-time? Playground

trait Foo {
    fn method(&self);
}

pub fn bar(foo: Foo) {
    foo.method();
}

IMO, for function bar, it doesn't need definition for Foo::method since the compiler doesn't know what's the type that implements Foo.

I checked, to make sure, and indeed the example:

trait Foo {
    fn method(&self);
}

trait Bar {
    fn method(foo: Foo);
}

compiles throughout just about any rust version (from 1.0.0 on), which probably includes versions that are older than any unsized local feature. This demonstrates that it is not any case of special rules for forward compatibility with trait objects.

For your code perhaps using the now standard notation with the dyn could help

pub fn bar(foo: dyn Foo) {
    foo.method();
}

In this case the compiler does now the type, it is a “trait object for the trait Foo”. (And the type dyn Foo is unsized, hence the error message when trying to pass it as a parameter).

Regarding the question of what’s a definition and what not: Everything that has a function body is a definition.

1 Like