Should weak be a type?

With all of that, you could make a property wrapper that duplicated most of the functionality of weak and unowned without compiler support. However, there is currently no generic constraint that can fully express "is any kind of class reference type"; the AnyObject constraint requires the type to have a simple object pointer representation, which precludes non-@objc class-bounded protocol and protocol-composition types. Therefore, that wrapper type — and the proposed Weak<T> and Unowned<T>, for that matter — would not be able to support the full range of types that you can use with weak and unowned today.

3 Likes

I want weak references to maintain reference-counted pointer to a side table, even after object is destroyed. This would enable to use address of the side table as a stable unique comparison key.

Stable - meaning it does not change when object is destroyed
Unique - it cannot be reused when new object is allocated if there are remaining weak references to the old one.

Currently when copying weak reference that contains non-nil pointer to the side table of the destroyed object, copy contains nil pointer to the side table. This leads to various equality paradoxes - references that used to be distinct became equal; reference and its copy may be non-equal.

That's a custom case of not being able to express existential subtyping as a generic constraint. I hope to include this to the GenericsManifesto by this MR.

You're confusing implementation details with semantics here quite a bit, but I think I can puzzle it out: you'd like to be able to create references to an object which don't keep it alive but do stay "valid" in the sense of never being confusable with a reference to any other object even after the object is destroyed. Those references would never be "resurrectable"; they'd just be usable as stable keys in dictionaries and so on. It turns out that we actually have most of the facilities we'd need for this already as part of the implementation of unowned references. How would you expect to use these?

I'm not sure I agree with this assessment.

I have often wanted this capability for storing references to observers in sets (or dictionaries when I need to associate a value with the observer).

Would you eventually want to somehow reclaim those entries when the observer is destroyed, or is the expectation that the entire object graph is being torn down at once? And wouldn't this need to be able to turn the reference back into a strong reference in order to notify the observer of something?

Basically, I'm wondering if what you really want is a weak set or dictionary, where entries semantically stop existing when the referent is destroyed.

Yes. The way I handle it now is to have a cleanup pass that looks for nild references. But ideally that wouldn’t be necessary.

Yes, the observer only gets notified when a strong reference can be successfully created.

Yes. I need the ability to associate a handler with the referent and sometimes a configuration value, so a dictionary keyed by the referent with entries that disappear when it is destroyed would be perfect. Ideally I could iterate and receive strong references to the referents that are still alive as well.

So we need to add an AnyObjectForReals super-constraint?

Okay. Yeah, I think a weak set or dictionary would be a great data structure to provide in the standard library. It might need additional runtime support to prune entries more efficiently than just the next time the structure is accessed, although having asynchronous modifications poses its own challenges.

5 Likes

Yes, and it would lose pretty much all of the representational advantages of AnyObject.

Awesome, @Nickolas_Pohilets would the semantics we discussed above meet your needs? Maybe a pitch can grow out of this thread.

Agree, what I do right now is just prune on access which is less than ideal.

I'm not sure I follow what you mean here. Can you elaborate?

Objects can be destroyed on any thread, so any system where that eagerly causes changes to a data structure means that those changes can race with other accesses to the data structure, meaning it needs to somehow be a concurrency-safe data structure.

1 Like

Oh, gotcha. Yeah, I guess there is a tradeoff to make there. I would expect contention to be very low in my use cases.

What would be the difference in the representations?

Presumably we would hide those differences from the user. AnyObject would smoothly convert to AnyObjectForReals, and the other direction would require an “as?”. But the bigger problem would be explaining the reason for having two similar types to the user (instead of AO working like AOFR); it’s mainly that we screwed up somewhere.

AnyObject says that the type has the representation of a single class reference, so the compiler knows its size and alignment and can pass it around in registers without needing to know the exact type. Furthermore, when used as a protocol type, AnyObject doesn't need to carry a type pointer around, since a dynamic type can always be recovered from the class instance.

AnyObject is a necessary type (and constraint) for Objective-C interoperation. Even in pure Swift, if we only had the more general constraint, most programmers would be surprised at the cost of using it.

2 Likes

Speaking of weak and AnyObject, I encountered this before:

protocol X: AnyObject {}

struct Weak<T: AnyObject> {
    weak var value: T?
}

weak var x1: X? // okay
var x2: Weak<X> // error: 'Weak' requires that 'X' be a class type

There's perhaps a good reason for this error, but I couldn't figure it out. It surly has nothing to do with weak, but adding "weak as a type" would probably need to overcome this.

That’s the limitation I was referring to above; it’s what Daryle and I were talking about. Non-@objc class-bounded protocol types are subtypes of AnyObject but don’t conform to it.

2 Likes

Another way to call this: a permanent memory leak.

While I see the utility in language support for durable unique identifiers for objects, there are better ways to do it (e.g. have a two-word weak ref, containing a copy of the object's UUID in addition to the pointer)

1 Like

Cool, well that would itself be a nice improvement to the generics system. It will be exciting to see these pieces phase in over time!

-Chris

I also find it baffling that protocol types constrained by AnyObject don't conform to AnyObject. AnyObject doesn't require anything that would prevent such a conformance. I understand this is a result of ABI decisions more than anything fundamental. A stored value of type P & AnyObject is expected to carry around its conformance "subtype->P" in addition to the class reference representing its value, presumably just to avoid runtime calls. @objc protocols are only special because the ObjectiveC ABI already demands a single pointer, so there's no way to carry around conformances even if we wanted to.

I think it would be neat to have a protocol type like P & AsAnyObject to force an objc-like ABI and thus selectively force conformance to AnyObject without type erasure, but it sounds like a lot of plumbing to make that happen, and probably not what people in this thread are asking for.

What I suspect is being proposed instead is more like a T : Reference constraint that could be used to enforce an underlying reference counted value without imposing AnyObject conformance at all.

I’m working on an in-house reactive programming framework. Framework heavily relies on comparing inputs to decide if something should be recomputed or update propagation can stop.

And data that users feed into the framework may contain pretty much anything including weak references and closures. To facilitate framework usage we ship a collection of property wrappers that help compiler synthesize equality conformance for user types.

False positives in equality lead to bugs - if I were using ObjectIdentifier as a comparison key, there is a small chance that old object (some sort of delegate) would be destroyed, new one would be created at the same address, but framework would never propagate a reference to the new one.

False negatives lead to redundant updates - negativity affecting performance, but preserving correct behavior.

I think for most of the user use cases weak references don’t outlive the referent, up to some unaccounted dispatch_async. So typically weak is used instead of unowned as a defensive measure.