Calling methods with inout arguments from a generic method

Hi,

I'm currently porting binary file streaming C++ code to Swift, to allow the Swift application to load the C++ generated binary files, and for the life of me I can't find a way to reimplement the C++ ReadField function using Swift genericity :

import Foundation

class MyStream {
    func ReadFieldName( _ name: String ) {
    }

    func ReadFieldValue( _ value: inout Int ) {
    }

    func ReadFieldValue( _ value: inout Float ) {
    }

    func ReadField<T>( _ name: String, _ value: inout T ) {
        ReadFieldName( name );
        ReadFieldValue( &value );
    }        
}

In despair, I implemented this code snippet in the online playground, and even that generates a compilation error :

/tmp/A3B1A8E7-9DEF-4DDE-BACF-34AAFAD958F4.wbyRFr/main.swift:17:9: error: cannot invoke 'ReadFieldValue' with an argument list of type '(inout T)'
        ReadFieldValue( &value );
        ^
/tmp/A3B1A8E7-9DEF-4DDE-BACF-34AAFAD958F4.wbyRFr/main.swift:17:9: note: overloads for 'ReadFieldValue' exist with these partially matching parameter lists: (inout Float), (inout Int)
        ReadFieldValue( &value );
        ^

I don't see anything fancy here, and this simple design works flawlessly in C++.

The idea is that I implement a few ReadFieldValue functions one by one, and then the generic ReadField function calls the appropriate one depending on the second argument type, this way I can implement the ReadField logic only once.

Any idea why this code can't be compiled ?

For instance, the following code compiles fine :

import Foundation

class MyStream {
    func ReadFieldName( _ name: String ) {
    }

    func ReadFieldValue( _ value: inout Int ) {
    }

    func ReadFieldValue( _ value: inout Float ) {
    }

    func ReadField( _ name: String, _ value: inout Int ) {
        ReadFieldName( name );
        ReadFieldValue( &value );
    }   

    func ReadField( _ name: String, _ value: inout Float ) {
        ReadFieldName( name );
        ReadFieldValue( &value );
    }        
}

So after manually instancing the template functions, the code is perfectly valid !

I'm desperately in need of an expert advice here :frowning:

Swift generic is different from C++ template.

While C++ template defers the type checking and instantiation until after you call ReadField("field", &int), Swift type checks the generic function at the time it is declared. So when you declare func ReadField<T>(...) you need to make sure that it works for all possible types of T in the same way, not just Int and Float.

There are a few ways to achieve something similar to what you want to achieve.

  1. You can statically check the type:

    func ReadField<T>(..., field: inout T) {
      if let value = field as? Int {
        field = newValue as! T
      } else if let value = field as? Float {
        ...
      } else {
        fatalError("Unsupported type")
      }
    }
    
  2. You can dynamically dispatch the function, by making ReadFieldValue a protocol requirement, or class method.

    protocol FieldReadable {
      mutating func readField(from stream: MyStream) { ... }
    }
    extension Int: FieldReadable {
      mutating func readField(from stream: MyStream) { ... }
    }
    
    class MyStream {
      func ReadField<T: FieldReadable>(..., value: inout T) {
        value.readField(from: self)
      }
    }
    

Some note on the function names:

You probably want to utilize the argument now that you have it in Swift. You may also want to use lower camel case per Swift API Design Guideline. Maybe something like:

class MyStream {
  func readField(name: String) { ... }
  func readField(value: inout Int) { ... }
  func readField(name: String, value: inout T) { ... }
}

If the users write stream.ReadField("name", &value) in Swift likely it'll stick out like a sore thumb. Also, judging from the name, maybe you want to return the value instead?

class MyStream {
  func readField<T>(type: T.Type = T.self, named: String) -> T { ... }
}

Since you no longer need to take care of the memory ownership contract imposed by the caller.

1 Like

Thanks A LOT for your very detailed answer !!

Indeed I had no knowledge at all of this (huge) difference between C++ and Swift generics.

Since your explanations those compiler messages now make much more sense to me...

And your solution based on a protocol to further characterize the template type should perfectly do the job. This actually makes the code clearer, at the expense of just a bit more verbosity.

What mattered to me was to avoid duplicating the template code, so mission accomplished :)

About the style, I must agree with you.

While converting this legacy code to Swift, it would indeed be smarter to also adhere to the standard styling of the language. Old habits die hard... ;)

1 Like