Immutable value as inout argument

Why do we have this limitation:

func mutates(_ x: UnsafeMutablePointer<Int>) { ... }
func doesntMutate(_ x: UnsafePointer<Int>) { ... }

func bar(x: Int, y: Int) {
    mutates(&x)      // 🛑 Cannot pass immutable value as inout argument:
                     // 'x' is a 'let' constant 😀👍
    var x = x
    mutates(&x)      // ✅ 😀👍

    doesntMutate(&y) // 🛑 same error?! 😒👎
    var y = y        // feels odd I need this 😒👎
    doesntMutate(&y) // ✅
}

I would understand the error if "doesntMutate" was taking "UnsafeMutablePointer" but here it takes "UnsafePointer" and it is not possible to modify the passed argument anyway. It feels that I have to make a copy for no good reason, which might be trivial for Int, but for String / Array / etc there's a not so trivial cost even taking into account the COW optimisation (and some value types won't have COW optimisation!).

1 Like

The & operator is not a pointer-to operator, it's an inout operator, and separately there's an inout-to-pointer conversion that converts an inout parameter to a mutable pointer. There's a valid argument to be made that Swift should add this capability, but you can accomplish what you want using the built-in withUnsafePointer function.

withUnsafePointer(to: y) { pointerToY in
    doesntMutate(pointerToY)
}

This should work.

8 Likes

Yes, ideally this is done by Swift itself behind the scenes, just imagine doesntMutate takes several pointer parameters:

withUnsafePointer(to: a) { a in
    withUnsafePointer(to: b) { b in
        withUnsafePointer(to: c) { c in
            doesntMutate(a, b, c)
        }
    }
}

Found this old thread.

With SE-377 we could mark the to parameter of withUnsafePointer with borrowing, but borrowing is already the implicit ownership for that parameter. If internally withUnsafePointer does something that requires a copy of its parameter then that's unfortunate.

As an optimization the compiler may sometimes pass a borrowing argument by-value when the type is copyable and simple, like Int. But because withUnsafePointer<T> is generic over its to parameter, it's always going to pass an address; the borrowing ownership just prevents the caller from having to retain/copy the value before the address is formed.

I do not think it is a good idea to allow implicit casts of &var to an unsafe type like UnsafeMutablePointer.

1 Like

One could imagine a new function once variadic generics are finished that makes this a little more ergonomic.

withUnsafePointers(to: a, b, c) { aPtr, bPtr, cPtr in
  doesntMutate(aPtr, bPtr, cPtr)
}

Or even shorter:

withUnsafePointers(to: a, b, c, doesntMutate(_:_:_:))
1 Like

Just thinking out loud here, if they're all the same type this should already be doable:

func withUnsafePointer<T, Result>(to args: T..., body: (UnsafePointer<T>) throws -> Result) rethrows -> Result {
  return try args.withUnsafeBufferPointer { buf in
    return try body(buf.baseAddress!)
  }
}

Usage:

withUnsafePointer(to: a, b, c) { ptr in
  doesntMutate(ptr, ptr + 1, ptr + 2)
}

Granted, pointer arithmetic doesn't look as nice as multiple arguments.

1 Like

These workarounds are nice of course but they can't beat the simplicity of the more straightforward and concise:

func bar(x: Int, y: Int) {
    doesntMutate(&x, &y)
}

(or whatever the actual syntax we will land with, be it doesntMutate(x, y) or doesntMutate(^x, ^y), or etc.

1 Like