Is there an existing Protocol to constrain a generic parameter to only trivial types?

Which types in Swift are considered trivial types and is there a Protocol, or Protocol composition, to restrict a generate parameter to only trivial types.

Consider a generic function for writing values into a raw buffer:

func write<T>(_ value:T) {
  bufferPointer.storeBytes(of:value, toByteOffset:0, as:T.self)
}

The documentation for storeBytes<T>(of: T, toByteOffset: Int, as: T.Type) states that T must be a trivial type. However, the compiler doesn't seem to complain if I do something like this:

class Style {
  var color = NSColor.red
  var font  = NSFont.labelFont(ofSize:11)
}

let style = Style()
write(style)

Style isn't something I'd consider to be a trivial type that .storeBytes can properly store, yet there's no error.

So what would be an appropriate constraint on write<T>(_ value:T) where T... such that only trivial types are allowed?

Not currently. You could assert(_isPOD(T.self)) as a dynamic check.

1 Like

Do you think it would be a good idea to add that check to the storeBytes method when running in debug mode?

Yeah, I know @Andrew_Trick is also interested in exposing a proper Trivial layout type constraint that could be used in generics as well. That would allow for load and storeBytes to work reliably with unaligned addresses too.

9 Likes

In the meantime, you can make your generic function require that T conform to an empty protocol of your own devising, and than retroactively conform the types you want to support to that protocol.

protocol QWritable {
}

func write<T>(_ value: T) where T: QWritable {
}

extension Int: QWritable {
}

It’s a bit manual, but it’s safer.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

Agreed, but of the existing protocols related to "numbers", which ones would be best to use to support:

  • All Integer Types
  • All Float Types
  • All Bool Types

Are BinaryInteger and FloatingPoint the best options for the first two? What about for Bool or would it be better to just have a typed version of write(_ value:Bool) for that?

Rather than enter the numeric protocol labyrinth, I would just do this on concrete types. Adding a conformance is simple, and you don’t need to add them all up front; you can just ‘fault’ them in as you need them.

Oh, and btw, despite my example above, I’m not totally comfortable with conforming Int to this protocol. Remember that Int is pointer sized and, conceptually at least, the sending and receiving processes could have different pointer sizes [1].

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] On macOS this can’t happen because 32-bit macOS code runs on the old runtime, and Swift requires the modern runtime.

Fair enough. So you'd recommend something like this then:

protocol MyTrivialType {}

extension Bool : MyTrivialType {}

extension Float32 : MyTrivialType {}
extension Float64 : MyTrivialType {}

extension Int8   : MyTrivialType {}
extension Int16  : MyTrivialType {}
extension Int32  : MyTrivialType {}
extension Int64  : MyTrivialType {}

extension UInt   : MyTrivialType {}
extension UInt8  : MyTrivialType {}
extension UInt16 : MyTrivialType {}
extension UInt32 : MyTrivialType {}
extension UInt64 : MyTrivialType {}

And then based on your last comment, you would not define these:

extension Double : MyTrivialType {}
extension Float  : MyTrivialType {}
extension Int    : ByteCodingTrivialType {}

I'm ok with this solution, I was just originally curious if there was a more idiomatic way of doing it more Swiftly.

The size of Float and Double is not platform-dependent like Int is. Float32 and Float64 are just typealiases for Float and Double, respectively.

Good point. I'll just use Double and Float instead of their aliases.

I mean, it's not really important, but you could keep them like that. There's also Float80 (everywhere except Windows, I think?) and we'll probably get a Float16 at some point.

As that thread says, Float32/64 were supposed to be the "true" names and Float/Double were going to be aliases. Like I said, not important - just an interesting bit of history.

1 Like

There have been requests for an AnyValue automatic protocol, that is a counter to AnyObject. Your idea would be an AnyTrival automatic protocol (and would refine AnyValue?).

Any... would be the wrong prefix to use. That tends to be used to signify a type-erased supertype into which you can assign any of the subtypes, which isn't relevant here. The request is for a generic constraint (even if, coincidentally, you could also use it as en existential protocol).

1 Like

You're thinking when we could (still can?) use "class" to specify a class constraint:

protocol ForClassesOnly1: AnyObject {}  // we use nowadays for...
protocol ForClassesOnly2: class {}  // ...this, the old(?) syntax.

So now, you would prefer something like:

protocol ForValueTypesOnly1: any !class  // once "any" is a keyword
protocol ForValueTypesOnly2: any value  // two new keywords
protocol ForTrivialTypesOnly: any trivial value  // three new keywords

?

No – AnyObject serves two purposes: as a constraint, and as a type-erased container for any class instance. The Any... part comes from the latter use. That latter use doesn't apply to trivial types, so I am saying it would be inappropriate for the constraint to be AnyTrivial i.e. it should just be Trivial.

2 Likes

Even the name AnyObject is a bit of a wart, IMO. It would probably be more consistent to just call it Object.

1 Like

While that is typically true, there are other layout constraints which could act as a kind-of "supertype" (or superset, really) of trivial types. For example, compiler also supports exact-size and maximum-size trivial constraints. Would we do AnyTrivial(64)?

Apparently you can use them in @_specialize, and they actually work:

1 Like