skip to Main Content

Based on https://developer.apple.com/documentation/swift/optionset , I notice that it is not possible to have more than 64 members in OptionSet

For instance

struct Options: OptionSet {
    let rawValue: Int


    static let options0 = Options(rawValue: 1 << 0)
    static let options1 = Options(rawValue: 1 << 1)
    static let options2 = Options(rawValue: 1 << 2)
    static let options3 = Options(rawValue: 1 << 3)
    // ...
    // ...
    // ...
    static let options63 = Options(rawValue: 1 << 63)
    
    // rawValue = 0
    static let options64 = Options(rawValue: 1 << 64)
    // rawValue = 0
    static let options65 = Options(rawValue: 1 << 65)
}

let options: Options = [.options64]

precondition(!options.contains(.options65))

For the above code example, the set shouldn’t contain option65 as it is not inserted initially.

However, since both 1 << 64 and 1 << 65 ended up as 0, this will yield wrong result.

I was wondering, is this the limitation of OptionSet of not able to have more than 64 members? Or, there is a workaround?

2

Answers


  1. Although Swift only provides fixed width integer types for up to 64 bits, OptionSet only requires that RawValue to be any kind of FixedWidthInteger for the default SetAlgebra implementations to be synthesised.

    When creating an option set, include a rawValue property in your type declaration. For your type to automatically receive default implementations for set-related operations, the rawValue property must be of a type that conforms to the FixedWidthInteger protocol, such as Int or UInt8.

    Therefore, you can technically always create your FixedWidthInteger of arbitrary size, by combining smaller integers, and use it in an option set.

    For example, here is a UInt128 implementation I found. I can use it in an option set just like normal:

    struct Foo: OptionSet {
        let rawValue: UInt128
        
        static let option1 = Foo(rawValue: 1 << 0)
        static let option2 = Foo(rawValue: 1 << 1)
        static let option3 = Foo(rawValue: 1 << 2)
        // ...
        static let option65 = Foo(rawValue: 1 << 64)
    }
    
    print([Foo.option1, .option65] as Foo) // Foo(rawValue: 18446744073709551617)
    

    Though obviously, this is not going to be as fast as the natively supported integers, and admittedly, if someone hasn’t already implemented the size you want, implementing a whole FixedWidthInteger just for an OptionSet isn’t very practical.

    Login or Signup to reply.
  2. An OptionSet needs to implement the methods

    mutating func formUnion(_ other: Self)
    mutating func formIntersection(_ other: Self)
    mutating func formSymmetricDifference(_ other:Self)
    

    from the SetAlgebra protocol, and the operator

    static func == (lhs: Self, rhs: Self) -> Bool
    

    from the Equatable protocol.

    If the rawValue is a type that conforms to the FixedWidthInteger protocol then the standard library provides default implementations for all these requirements. So one option (as suggested by Sweeper) is to use an integer type with more than 64 bits.

    But actually not the full power of FixedWidthInteger is needed for OptionSet, only the bitwise AND, OR, and XOR operations. So here is another possible solution:

    First define a protocol for the required bitwise operations:

    protocol BitwiseOperators {
        static func | (lhs: Self, rhs: Self) -> Self
        static func & (lhs: Self, rhs: Self) -> Self
        static func ^ (lhs: Self, rhs: Self) -> Self
        static var allZeros: Self { get }
    }
    

    (Remark: There used to be a BitwiseOperation protocol in Swift 3, but that does not exist anymore.)

    Next we define default implementations for all required OptionSet methods if the raw value conforms to the BitwiseOperators type:

    extension OptionSet where RawValue: BitwiseOperators & Equatable {
        
        init() {
            self.init(rawValue: RawValue.allZeros)
        }
        
        mutating func formUnion(_ other: Self) {
            self = Self(rawValue: self.rawValue | other.rawValue)
        }
        
        mutating func formIntersection(_ other: Self) {
            self = Self(rawValue: self.rawValue & other.rawValue)
        }
        
        mutating func formSymmetricDifference(_ other:Self) {
            self = Self(rawValue: self.rawValue ^ other.rawValue)
        }
        
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.rawValue == rhs.rawValue
        }
    }
    

    Now, in order to define an option set with more than 64 possible options, we need a raw value type which can store the necessary number of bits, and implements the AND, OR, and XOR operations. Here is an example for 128 bits, but it can easily be extended to any necessary size:

    struct Raw128: Equatable, BitwiseOperators {
        let lo: UInt64
        let hi: UInt64
        
        static func | (lhs: Raw128, rhs: Raw128) -> Raw128 {
            Raw128(lo: lhs.lo | rhs.lo, hi: lhs.hi | rhs.hi)
        }
        
        static func & (lhs: Raw128, rhs: Raw128) -> Raw128 {
            Raw128(lo: lhs.lo & rhs.lo, hi: lhs.hi & rhs.hi)
        }
        
        static func ^ (lhs: Raw128, rhs: Raw128) -> Raw128 {
            Raw128(lo: lhs.lo ^ rhs.lo, hi: lhs.hi ^ rhs.hi)
        }
        
        static var allZeros: Raw128 { Raw128(lo: 0, hi: 0) }
    }
    

    As a convenience we can define an initializer which sets exactly one bit at a given position:

    extension Raw128 {
        init(bitPos: Int) {
            switch bitPos {
            case 0..<64:    lo = 1 << bitPos; hi = 0
            case 64..<128:  lo = 0; hi = 1 << (bitPos - 64)
            default: fatalError("`bit` must be in the range 0...127")
            }
        }
    }
    

    Finally we can define the Options options set with Raw128 as the raw value type:

    struct Options: OptionSet {
        let rawValue: Raw128
    
        init(rawValue: Raw128) {
            self.rawValue = rawValue
        }
        
        static let options0 = Options(rawValue: RawValue(bitPos: 0))
        static let options1 = Options(rawValue: RawValue(bitPos: 1))
        static let options2 = Options(rawValue: RawValue(bitPos: 2))
        // ...
        static let options63 = Options(rawValue: RawValue(bitPos: 63))
        static let options64 = Options(rawValue: RawValue(bitPos: 64))
        static let options65 = Options(rawValue: RawValue(bitPos: 65))
    }
    

    and everything works as expected:

    let options: Options = [.options64]
    print(!options.contains(.options65)) // true
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search