Generic conversion between signed and unsigned integer types

Let's assume that I have a generic function foo() which returns an unsigned integer whose type is inferred from the context:

func foo<T: UnsignedInteger>() -> T {
    return 0 // Simplified example :)
}

let u: UInt = foo()
let u16: UInt16 = foo()

Now I want to define a version for signed integers which calls foo() for the corresponding unsigned integer type, and returns the signed integer with the same bit pattern. In other words,

let i: Int = foo()

should be “equivalent” to

let i = Int(bitPattern: foo())

This is one of my failed attempts:

func foo<T: SignedInteger>() -> T {
    return T.init(bitPattern: foo())
    // Incorrect argument label in call (have 'bitPattern:', expected 'integerLiteral:')
}

and I wonder if this is possible at all. Am I overlooking something obvious?

1 Like

Is init(bitPattern:) a part of SignedInteger protocol?

It seems that init(bitPattern:) is only defined for all (concrete) integer types, but not part of some protocol, that would be a problem for what I try to achieve.

Would this solution work for you?

func foo<T: UnsignedInteger>() -> T {
    return 0 // Simplified example :)
}

protocol BitPatternInitializable {
	associatedtype BitPattern: UnsignedInteger
	init(bitPattern: BitPattern)
}

// every type you want to work with
extension Int: BitPatternInitializable {}
extension Int16: BitPatternInitializable {}

func foo<T: BitPatternInitializable>() -> T {
    return T.init(bitPattern: foo())
}
1 Like

Apart from the fact that you have to declare all the required conformances explicitly – yes.

Perhaps my error is to assume that every signed integer type has a corresponding unsigned integer type (and vice versa).

The closest to that would be Magnitude, which is a type guaranteed to be large enough to store the absolute value of any instance of its Numeric type. It's not guaranteed to be unsigned though, but for all of the built-in integer types it is.

How about:

func foo<T: UnsignedInteger>() -> T {
    return 0
}

func foo<T: SignedInteger> -> T where T.Magnitude: UnsignedInteger {
    let bitPattern: T.Magnitude = foo()
    return T(truncatingIfNeeded: bitPattern)
}
2 Likes

The only place this gets a bit wonky is that it's not guaranteed that a "corresponding signed integer type" exists. It does for all the concrete stdlib types, but it's not guaranteed to exist for an arbitrary type conforming to UnsignedInteger.

What will probably work for your purposes is:

func foo<T: SignedInteger>() -> T where T.Magnitude : UnsignedInteger {
  return T(truncatingIfNeeded: foo() as T.Magnitude)
}

This will fall over for a hypothetical non-fixed-width type that it its own magnitude, but will work just fine for all the stdlib types, and more generally for types conforming to FixedWidth & SignedInteger (no fixed-width signed-integer type can possibly be its own magnitude, because the semantics of FixedWidth imply a two's-complement range of values).

Edit: I see that @Nobody1707 gave exactly this solution already. Hopefully my explanation of why it works this way is helpful.

4 Likes

@scanon @Nobody1707 That would indeed work for all integer types from the standard library. Thanks for all responses so far!

Could – theoretically – T.Magnitude have a larger bit width than T?

I can't find the guarantee you are looking for:

  • Magnitude (documentation of the associated type)
  • Magnitude (declaration of the associated type)
  • magnitude (documentation of the property)

The current implementation of abs(_:) does not make any further assumption.

I suppose that a nice conforming type would use its precise unsigned counterpart as its Magnitude, though. You may safely rely on it.

But is it needed? Couldn't using intermediate IntMax/UIntMax values in your conversion functions be another way to solve your puzzle?

That may be, but I don't yet see how. For a signed integer type T, foo<T>() should call foo<U>() where U is the unsigned counterpart with the same bitwidth as T.

I'll have to think about it ...

This is where you get into trouble. Conformance to SignedInteger & FixedWidthInteger does not imply that an "unsigned counterpart" exists. It always does for stdlib types, but someone could absolutely implement their own Int73 that doesn't have a corresponding UInt73. This is intentional.

The thing that @Nobody1707 and I sketched will work for all non-pathological cases like this. If you want to handle everything with full generality, you'll need to define your own protocol that provides the constraint you want, and add retroactive conformances for the stdlib types.

// Define a protocol that requires a "corresponding signedness" type exists.
public protocol MartinsInteger : FixedWidthInteger { 
  associatedtype OtherSignedness : FixedWidthInteger 
}

// Retroactively conform some signed types to the new protocol.
extension Int : MartinsInteger { public typealias OtherSignedness = UInt }
extension Int8 : MartinsInteger { public typealias OtherSignedness = UInt8 }
extension Int16 : MartinsInteger { public typealias OtherSignedness = UInt16 }
extension Int32 : MartinsInteger { public typealias OtherSignedness = UInt32 }
extension Int64 : MartinsInteger { public typealias OtherSignedness = UInt64 }

// Now you can define your function for signed types, conforming to the new protocol.
func foo<T>() -> T where T: MartinsInteger, T.OtherSignedness: UnsignedInteger { 
  return T(truncatingIfNeeded: foo() as T.OtherSignedness) 
}

foo() as Int8 // works

For large classes of specific functions foo, however, there are likely to be simpler specific solutions. What are you really trying to do?

I'll respond in more detail later. – Is your last solution comparable to the protocol BitPatternInitializable that @cukr suggested at Generic conversion between signed and unsigned integer types - #4 by cukr above?

@cukr's solution adds a new protocol where conformance means a type can be initialized from a bit pattern represented as an unsigned integer.

My solution adds a new protocol where conformance means that a fixed-width integer type has a "corresponding type with opposite signedness".

Mechanically what they look like in your program is almost identical, but the sets of types that make sense to conform to these two protocols is different (@cukr's suggestion will "work" for arbitrary types that you want to conform, not just integers. That may or may not make sense, depending on the semantics of foo()) and the sent of operations that make semantic sense to do with these operations are different.

Fair question, and sorry for not responding earlier. I was just playing around with some simple (de)serialization code, something like

func read<T: FixedWidthInteger & UnsignedInteger>(from data: Data) -> T {
    var value: T = 0
    for idx in 0..<MemoryLayout<T>.size {
        value = value << 8
        value += T(data[idx])
    }
    return value
}

Here bit shifting on unsigned integers is used to read the value in big-endian order, not relying on the data being aligned properly for type T. Then I tried to define a corresponding
function for signed integers

func read<T: FixedWidthInteger & SignedInteger>(from data: Data) -> T {
    // ...
}

which reads the unsigned value first and then does the bit pattern conversion.

All solutions proposed in the above replies can be used to solve this, thanks a lot.

Did something new happen in the past year and a half to make it this simple?

public extension BinaryInteger {
  /// The bits of this integer, in an unsigned variant.
  var bitPattern: Magnitude { .init(truncatingIfNeeded: self) }
}

I know there aren't really enough constraints to make that description true, but it's truthy enough for me for now. Is there anything else new which makes the extension unnecessary?

That has always "worked" since Swift 4 (specifically SE-0104). The only real issue is that Magnitude of an arbitrary type conforming to BinaryInteger may not actually be an unsigned type--it could be Self, or some other type (for FixedWidthInteger, it's guaranteed to be unsigned).

The more subtle issue for FixedWidthInteger is that it will be an unsigned integer type, but it may not be the one you want it to be; the OP asked about "the corresponding unsigned integer type", which may not exist, or may not be bound to Magnitude. A hypothetical Int57 might use UInt64 as its Magnitude, and that would be fine.

4 Likes