Working with identity (comparing equality of references/pointers)

Changing only the type but not the method body works precisely because of deref coercion. If your method is declared to return &<T as Deref>::Target, then if your method body actually returns a &T, it will be converted to &<T as Deref>::Target.

2 Likes

Ah, function results are a coercion site. Thanks!

Good point, so long as Pointee is an automatic trait which can not be overridden, the set of possible NonDyn traits are known. This makes it not only something useful to Rust programmers in their own projects, but something the language could guarantee is accurate.

At which point, some of the special-casing of Sized with regards to dyn Trait could be replaced with NonDyn, and NonDyn would become a super-trait of Sized for backwards compatibility. (This is the main part that needs fleshed out in more depth IMO.)

In the context of RFCs, I could imagine some hesitancy around the indirectness and having a core trait be NonSomething, but I'm not on any teams, so who knows.

Even if a fourth type of metadata was added later, that wouldn't be a huge deal-breaker I guess? Then the semantics of NonDyn would be more like SizedOrSlice (e.g. a fourth type of metadata would not be covered by the NonDyn aka SizedOrSlice trait until the respective code was updated).

Like I said, if it's named SizedOrSlice, then it doesn't contain any Non… :innocent: Not sure if it sounds better though.

Why would that be necessary? Because if I guarantee a type to be Sized, it should also be guaranteed to be NotDyn? Wouldn't that happen automatically? Not sure if I overview it correctly.


Update: Ah, got it. This doesn't work if debug_not_dyn expects a NonDyn:

fn wrapper<T: Debug>(reference: &T) {
    debug_not_dyn(reference)
}

And T being Sized implicitly should guarantee it, but the compiler doesn't know it.

Trying to define the following causes a conflict:

impl<T: Sized> NotDyn for T {}

My thinking was around dyn safety, where you can have a method like:

fn foo(self) where Self: Sized;

And the trait will still be dyn safe as a whole because this dyn unsafe method is not callable for (non-Sized) trait objects. However, this is a poor substitute in some cases, like perhaps

fn bar<T: AsRef<()>>(&self, t: T) -> i32 where Self: Sized;

which might make perfect sense to implement for str or [T], but is still not dyn safe due to the generics. But if instead the dyn safety logic was based on NonDyn, we could change this to:

fn bar<T: AsRef<()>>(&self) -> i32 where Self: NonDyn {
  // If you're going to make this change to an existing trait
  // you'll have to add a default body, in case the trait was
  // implemented for DSTs already (which could exclude this body).
  // (But don't have to exclude it!  Even though it was uncallable.)
  0
}

And the trait would still be dyn safe, but we could also now call the method for str and [T]. However, our old version of bar (with the Sized bound) has to remain dyn safe for backwards compatibility. Moreover, implementations (including generic ones) which specified where Self: Sized on the method have to keep compiling to ensure backwards compatibility.

Likewise, trait Trait: Sized has to continue to inhibit dyn safety, and generic implementations bound on Sized have to keep compiling.

I guess it's not strictly necessary to be a super-trait so long as the compiler logic keeps taking Sized into account in all of these situations, but it seems much cleaner to me to add the super-trait so that Sized directly implies NonDyn as per existing super-trait behavior. And just generally speaking, reasoning about dyn safety shouldn't care about Sized per se.

There's also the whole possibility of some ability to pass/return DSTs coming to Rust some day; that's an aspect I haven't tried to dive into. At a minimum it's another situation where Sized will probably become the wrong tool for the job (and we'll then yearn for a StackPassable trait or something).

2 Likes

Just because I stumbled upon it, I wanted to share the following:

Apparently, even the Rust compiler itself uses the concept of defining equality through reference equality (aka "identity" or pointer equality) in some cases, see impl PartialEq for rustc_middle::ty::TyS<'_>, for example.

(Found this when following the link in this post.)

In other words, using the Sized bound to enable dyn safety is semantically wrong and unnecessarily reduces generalizability. Haven't thought on that before, but it makes sense.

That works because a type representation is never zero-sized unless your language only has a single type (in which case it would always be equal to itself and you wouldn't need to represent it anyway).

1 Like

To be clear, I agree there are use cases for a concept of "object identity" and there are also ways to implement that using pointer comparisons. The point of my earlier post is not that this is a fool's errand or the goal is useless, but that this is simply not a native concept to Rust and you should expect to do some work to marry the concepts because the language hasn't done it for you (unlike some other languages).

Part of that work may certainly be defining what exactly you mean by "identity" for things like unsized and zero-sized types. If you want to be generic over "all types" (whatever that means) then you often have to do quite a bit more work than if you can simply deal with one type as in the example you give from rustc.

1 Like

I think that is necessary but not sufficient. There need to be certain guarantees regarding stability of pointer values for that to work. It's not only a ZST issue.

Yeah, I see. There seem to be a lot of caveats.

That is the high-level part, yes. But it also makes me wonder how (and where) the low-level pointer comparison is defined in Rust. I think it's just bitwise comparing the pointers, but is that written down somewhere?

Unless the latter is clearly defined (and certain stability/uniqueness rules are known), we can't judge about whether pointer comparison is or isn't a useful approach for whatever definition of "identity".

What seems clear now is that in the general case, pointer comparison may have different rules than what would be useful for an "identity" op. In cased of Sized types, the issue doesn't seem to be the pointer comparison tough, but the question when (base) addresses are known to be equal or not equal at all. And that isn't trivial not just in the case of ZSTs but may also cause problems in other cases (as shown in some examples in the thread above).

However, I do believe that for non-zero sized values on the heap that are Sized, things are quite safe.

You also need to add the caveat that both values are of the same type: A struct and its first member can both be Sized and reside at the same address. They may even have the same size if there are no other members of the struct.

1 Like

Yes, you are right. But that's easy to guarantee by capturing the original type, see current RefId type that is dependent on T (and thus also captures the lifetime if T is a reference.

Rc and owning smart pointers in general (which are used in that context AFAICT) unconditionally heap-allocate. Those addresses aren't moving around.

Can the pointer held by a regular & reference to a stack allocated local variable change during the lifetime of that reference? I don't think so. If it can, I'd like to see example code demonstrating it.

I don't think so, but the point is that with a smart pointer, you can move around ownership conceptually, while preserving the address. I.e. the following is guaranteed to pass the assertion:

let rc_before = Rc::new(42);
let ptr_before = &*rc_before as *const i32;
let rc_after = rc_before;
let ptr_after = &*rc_after as *const i32;
assert_eq!(ptr_before, ptr_after);

Meanwhile, the following isn't:

let val_before = 42;
let ptr_before = &val_before as *const i32;
let val_after = val_before;
let ptr_after = &val_after as *const i32;
assert_eq!(ptr_before, ptr_after);

Here, there is no reference existing anymore though, right? (Or does the temporary &val_before not get dropped until the end of the whole block?)

I think the reference created by &val_before is a temporary, so it only lives as long as the immediately enclosing expression. However, even if it did exist until the end of the scope, the code would compile, since the borrow isn't actually used after it's converted to a raw pointer.

I think so too. Otherwise the following code should also not be able to compile:

let mut val = 42; 
let ptr1 = &mut val as *mut i32; 
let ptr2 = &mut val as *mut i32;

Anyway, let val_after = val_before; also performs a copy, so it's no surprise that the pointers aren't equal, and it's not what @tczajka referred to:

I would assume (and hope) it can't change.

See also @quinedot's post above.

I get that, but it is exactly my point that this is not relevant, since we were talking about smart pointers and passing on ownership while having a pointer to an object. With regular references, that is not possible, so that simply does not apply here.

That makes me realize there is a method called Rc::ptr_eq, which basically is that sort of "identity" comparison, but limited to this particular smart pointer. Maybe there could instead be a trait to generalize this? Like:

trait Identity {
    fn ptr_eq(&self, other: &Self) -> bool;
}

But that doesn't help us to add these to a HashMap or HashSet. I guess we'd rather need something like a method that extracts the (stable) address.


On the other hand, I don't think smart pointers are the only use case where "identity" makes sense. For example when taking slices from a Vec, it should also be safe to compare the pointers (apart from the zero-size case).