Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow omission of unsatisfiable members in trait impls #2829

Open
alercah opened this issue Dec 3, 2019 · 10 comments
Open

Allow omission of unsatisfiable members in trait impls #2829

alercah opened this issue Dec 3, 2019 · 10 comments

Comments

@alercah
Copy link
Contributor

alercah commented Dec 3, 2019

Currently, where bounds on trait methods are largely only useful for one purpose, which is to make traits object-safe. In particular, if we write trait Foo { fn foo(self) where Self: Sized; } rather than trait Foo: Sized { fn foo(self); }, then dyn Foo is a valid type, albeit one where we cannot call foo due to the Sized constraint. If the Sized constraint is directly on Foo, however, then since dyn Foo: !Sized, we get a constraint that dyn Foo: Sized, which is impossible.

Interestingly, this lets us make a value of type (dyn Foo) implementing a trait (Foo) but on which a method (foo) cannot be called due to its unsatisfied constraint (dyn Foo: Sized).

As far as I can tell, this is the only such example. If I try to implement it for another unsized type, I either get an error about foo not being implemented or get an error about the constraint being unsatisfied. But it's clearly not an inherent requirement that foo be implemented, because dyn Foo has no foo implementation, just a surface-level one.

Note that you can define a default implementation, and then you get a similar result, with the resulting method not being callable on unsized types like str. But then you have to provide a default implementation for all types, which is in many practical examples just going to be an "impossible" panic, but then you cannot force every implementor to override it, meaning that this impossible panic could leak into real code.

Thus, an idea of allowing an impossible constraint to be ignored. Here's an example borrowing a C++ syntax:

trait Foo {
    fn foo(self) where Self: Sized;
}

impl Foo for str {
    fn foo(self) where Self: Sized = delete;
}

In this example, the = delete tells the compiler that we deliberately want to not implement the function, and the compiler in this cause ought to see that str: !Sized and permit the impl. This should only be permitted where the compiler can confidently negatively reason, in the same way as when calculating impl overlap. Orphan rules make this viable in many cases.

I'm not sure that this feature is incredibly useful---the primary use case I can think of it is to allow specialization of things with additional trait constraints, but in that case you probably have a default implementation anyway. The other thing that comes to mind is trying to ease version migrations, to let you add non-default methods that are conditioned on the version of implementing types, but you'd still need to propagate the constraint which would largely undermine its utility.

@comex
Copy link

comex commented Dec 3, 2019

Instead of adding special syntax for = delete, perhaps just allow omitting functions from impls if they have unsatisfiable bounds?

@alercah
Copy link
Contributor Author

alercah commented Dec 4, 2019

My spider sense should it should have a special syntax, but other than that I have no strong opinion one way or the other.

@crlf0710
Copy link
Member

crlf0710 commented Dec 4, 2019

This is duplicate of rust-lang/rust#57900, i think.

@burdges
Copy link

burdges commented Dec 4, 2019

I suspect specialization needs the syntax because otherwise specialization drops methods freely. If so, then maybe

impl Foo for str {
    fn foo(self) where Self: Sized = !;
}

Ignoring that we probably would not want this, would this work?

pub trait Default {
    fn default() -> Self where Self: Sized;
    unsafe fn default_unsized(self: *mut Self)
        { self.write(Self::default()); }
}

impl<T: Default> Default for [T] {
    fn default() -> [T] = !;
    unsafe fn default_unsized(self: *mut [T]) {
        // We have the length information present since
        // ::std::mem::size_of::<*mut [u64]>() equals ::std::mem::size_of::<&[u64]>()
        // but maybe some unsoundness exists unless we employ
        // another formulation like &mut MaybeUninit<[T]>.
        // I suppose &mut [MaybeUninit<T>] breaks this example.
    }
}

@alercah
Copy link
Contributor Author

alercah commented Dec 4, 2019

I don't see any reason it would. The signatures don't match.

@RustyYato
Copy link

RustyYato commented Dec 4, 2019

@burdges This can't work because default has a Sized bound and default_unsized does not. This means that default_unsized can't call default

@burdges
Copy link

burdges commented Dec 4, 2019

Right, makes sense. I suppose some more explicit use of specialization gets closer, probably still not quite. I'm not actually suggesting Default should provide such a method, just curious.

default impl<T: Default+Sized> Default for T {
    unsafe fn default_unsized(self: *mut T) { .. }
}

@RustyYato
Copy link

That would work, (although your signature is messes up it should be unsafe fn(self: *mut T))

@burdges
Copy link

burdges commented Dec 5, 2019

As an aside, it might not work in practice because, although a *mut T with T: !Sized has the size information at runtime, I do not see any stable way to access this information.

@canndrew
Copy link
Contributor

This is also a dupe of rust-lang/rust#20021

There's also this semi-related RFC: #1699

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants