Should weak be a type?

Would Set<Weak<T>> automatically remove deallocated objects?

No; weak-keyed sets and dictionaries really have to be their own dedicated types. And as I cover in that design document, there are several different reasons you might want something like a weak-keyed or weak-valued dictionary, and they lead to quite different designs.

I’ve always thought of reference being a synonym for the pointer (with the exception of the C++, which has both, and distinguishes between them). And I would expect equality of references to be defined in terms of object identity - two references being equal if they refer to the same object.

I really dislike separation between == and ===. I don’t think any class should implement the == operator. And those who do now should be made structs with some heap-allocated storage encapsulated inside. I’ve already ranted about this here - Equality of functions - #32 by Nickolas_Pohilets

Anyway, regardless of terminology, for my use case I really need a type that represents a non-nil reference-counted pointer to a side table. And I need that type to not be nillified on copy. @John_McCall, do you remember what was the reasoning for making weak references nullified on copy?

1 Like

Weak references are nullified when the object is destroyed. What do you want to happen when the object is destroyed?

John, could weak and unowned becomes standard library types when we have support for move-only, non-movable types, rule-of-5, and other fancy things as part of ownership phasing in?

1 Like

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.