Swift 5.7/6 and Non-Final Class Opt-In to AtomicReference

Consider a class Cls, to which we'd like to be able to make an atomic reference via ManagedAtomic. This class is intentionally the root of a class hierarchy, and we'd like to be able to make references to it, and its subclasses:

import Atomics

// Swift 5.6
class Cls: AtomicReference {} // ✅

This works without complaint in Swift 5.5/5.6, but in Swift 5.7, new warnings are introduced by the conformance above:

// Swift 5.7
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicOptionalWrappable', which requires that 'Self.AtomicOptionalRepresentation.Value' is exactly equal to 'Self?'; this is an error in Swift 6
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicReference', which requires that 'Self.AtomicOptionalRepresentation' is exactly equal to 'AtomicOptionalReferenceStorage<Self>'; this is an error in Swift 6
// warning: Non-final class 'Cls' cannot safely conform to protocol 'AtomicValue', which requires that 'Self.AtomicRepresentation.Value' is exactly equal to 'Self'; this is an error in Swift 6
class Cls: AtomicReference {} // ⚠️

These warnings make perfect sense: as written, the constraints on the Atomic* types all require type equality, which will clearly be violated by subclasses. (In practice, I don't believe this matters, because as stored, Cls and its subclasses all have identical layouts—just a pointer to actual storage—but happy to be corrected if this is a nontrivial mistake.)

Is there guidance going forward on what to do here?

  • Will an exception somehow be made for these types, or will clients of swift-atomics no longer be able to do this?
    • If this is safe to do, will there be a way to silence these warnings in Swift 5.7? (Apologies if this is already possible, and I've missed that capability)
  • And if not, what's the right way to handle this? Should we be writing a generic final class AtomicWrapper<T: AnyObject>: AtomicReference to handle this type of storage, and make references to that? Drop down to AtomicReferenceStorage/AtomicOptionalReferenceStorage directly? Something else?

Apologies if there is already official guidance somewhere on how to handle this — in my searching, I haven't found anything definitive.

@lorentey, I'm wondering if there's a preferred approach here going forward — any guidance on what might be appropriate to do?

(Sorry if you're not the right person to ask; reaching out to you as apparent primary dev / package maintainer.)

Huh, interesting!

The diagnostic certainly seems correct, and the workaround probably best belongs in the Atomics package. AtomicReference already has a fixme about a compiler issue (https://bugs.swift.org/browse/SR-10251) that may have the same root cause. :thinking:

One way to go at it might be to add a new associated type to the AtomicReference mixin protocol, along the lines of:

public protocol AtomicReference: AnyObject, AtomicOptionalWrappable
  associatedtype AtomicBaseClass: AtomicReference = Self
    Self: AtomicBaseClass,
    AtomicRepresentation == AtomicReferenceStorage<AtomicBaseClass>,
    AtomicOptionalRepresentation == AtomicOptionalReferenceStorage<AtomicBaseClass>

(I don't know if this shape is currently expressible within the type system; I expect the requirements will need to be tweaked a bit.)

1 Like

(I filed https://github.com/apple/swift-atomics/issues/53 to track this issue.)

1 Like

For what it's worth, I don't see a reason why it wouldn't be possible to make an atomic strong reference to the root of a class hierarchy -- the underlying atomics machinery is expressed on AnyObject and it is happy to work with any strong reference.

The protocol is only needed to make these play well with the ManagedAtomic/UnsafeAtomic constructs, and to provide a minimal amount of control over what classes are expected to be used this way.

The primary limitation with applying this to a class hierarchy is that ManagedAtomic<Cls> would always return a Cls, never a subclass type -- but then again, that's also true for regular variables.

For a subclass of Cls called Derived, I think the approach above would also allow ManagedAtomic<Derived> to work, although the code might need to be a bit careful about always downcasting to the correct type. (Subclasses won't be able to customize their atomic representations, but that's not much of a hardship -- I don't expect anyone would want to roll their own AtomicRepresentation type, anyway.)

1 Like

Fantastic — thanks for this, Karoy! I'm hoping the solution won't be too painful to express in the type system.

Thanks for confirming! I figured this was the case, and that the issue was expressing the right constraints to the compiler; sounds like we're on the same page.

Exactly, and in my case, downcasting to the right type won't be an issue. (I do expect to store ManagedAtomic<Derived> much more frequently than ManagedAtomic<Cls>, but that we'll get for free.)

Given some spare time, I'll see if I can poke about with the specific spelling of the constraints. Hopefully I can be of some use.

1 Like