Non-Copyable types and function overload resolution

I recently discovered that the following two methods conflict.

  • func write<T>(_ value: borrowing T) where T: StringConvertible & ~Copyable
  • func write<each T: StringConvertible>(_ value: (repeat each T))

It may be that ~Copyable in the method signature interferes with the compiler's choice, resulting in the following error in overload resolution:

# swift build
error: ambiguous use of 'write'
write(Foo())
^
note: found this candidate 
func write<T>(_ value: borrowing T) where T: StringConvertible & ~Copyable {
     ^
note: found this candidate 
func write<each T: StringConvertible>(_ value: (repeat each T)) {

There are currently 2 methods:

  1. Remove ~Copyable: Not what I want.
  2. Add @_disfavoredOverload to the tuple version: Force the compiler to lower the priority of this method, which can achieve the goal, but uses underscore attributes, which is not perfect.

I guess the method conflict might be a compiler bug?

Full code
protocol StringConvertible: ~Copyable {
    var description: String { get }
}

struct Foo: StringConvertible {
    var description: String { "42" }
}

func write(_ value: some CustomStringConvertible) {
    print(value.description)
}

// or: func write(_ value: some StringConvertible)
func write<T>(_ value: T) where T: StringConvertible {
    print(value.description)
}

func write<each T: CustomStringConvertible>(_ value: (repeat each T)) {
    for item in repeat each value {
        print(item.description, terminator: ",")
    }
    print()
}

func write2<T>(_ value: borrowing T) where T: StringConvertible & ~Copyable {
    print(value.description)
}

// @_disfavoredOverload
func write2<each T: StringConvertible>(_ value: (repeat each T)) {
    for item in repeat each value {
        print(item.description, terminator: ",")
    }
    print()
}

write(100)
write(Foo())
write((1, 3, 5))
write2(Foo()) // ❌
write2((Foo(), Foo(), Foo()))

Xcode version: 16.4 (16F6)

This doesn't have anything to do with Copyable at all, I don't think:

func write<T>(_ value: T) { }
func write<each T>(_ values: (repeat each T)) { }

write(1) // Ambiguous use of 'write'

The problem is that write(1) can satisfy both overloads, which definitely feels unexpected here (maybe a historical quirk?)

Either way, you can disambiguate these two overloads by making the tuple one explicitly accept two or more values:

func write<T>(_ value: T) { }
func write<T, U, each V>(_ values: (T, U, repeat each V)) { }

write(1) // uses first overload
write((1, 2, 3)) // uses second overload

It's not surprising that there are ambiguous in bare methods.

My methods are constrained, and tuples can not implement any protocol in Swift (at least for now), so these methods are unambiguous when '~Copyable' is not added (see full codes).

My problem is that after adding '~Copyable', there should be no ambiguity either. '~Copyable' may just be a trigger, not a root cause.

It can satisfy both overloads because in isolation, either one will work (try it). If an expression involves overloads, the type checker will find all valid solutions and compare them to pick the best one. The behavior is that "by default", two solutions are ambiguous unless an explicit rule has been implemented to disambiguate them.

In this case though, if you change the example slightly, it becomes unambiguous:

func write<T>(_ value: T) { }
func write<each T>(_ values: repeat each T) { }

write(1) // We prefer the first overload

Notice how we have a bare pack here, instead of a pack wrapped in a tuple.

It should be possible to expand the current rule to cover the tuple case as well; please file an issue :slight_smile:

2 Likes

If not an expansion of a current rule, I'd argue it's still justifiable (just by common sense) to add a standalone rule that overloads which take an argument T are "more specific" than overloads which as a degenerate case take a single-element tuple (T).

2 Likes

The only case where that occurs is with parameter packs. The "one-element tuple type" representation (T) resolves to the same type as T otherwise.

4 Likes

Thanks everyone, I think the current handling of single-element tuples is acceptable.
My real problem can be summed up as follows:

  1. bare method are ambiguous: fine.

    func write<T>(_ value: T)
    func write<each T>(_ values: (repeat each T))
    
  2. constrained method are NOT ambiguous: fine.

    func write<T>(_ value: borrowing T) where T: StringConvertible
    func write<each T: StringConvertible>(_ value: (repeat each T))
    
  3. constrained method with ~Copyable ARE ambiguous: bug or feature :cross_mark:?

    func write<T>(_ value: borrowing T) where T: StringConvertible & ~Copyable
    func write<each T: StringConvertible>(_ value: (repeat each T))
    
More
  1. single-element tuple (T) resolve as T: fine.

    func write<T>(_ value: borrowing T) where T: StringConvertible // 1
    func write<each T: StringConvertible>(_ value: (repeat each T)) // 2
    
    write(1) // call 1
    write((1)) // also call 1
    
  2. single T resolve as (T) : fine.

    func write<each T>(_ value: (repeat each T)) // 1
    
    write(1) // call 1
    write((1)) // also call 1
    
  3. bare method with parameter packs: fine.

    func write<T>(_ value: T) // 1
    func write<each T>(_ value: repeat each T) // 2
    
    write(1) // call 1
    write((1)) // also call 1
    write(1, 2) // call 2
    
1 Like

I went ahead and filed issue #82172 (crediting @mattcurtis).

2 Likes

I think I may understand. When I add ~Copyable, the compiler behaves the same as for bare methods, so there is ambiguous.

If we can solve #82172, then we can solve my problem, right?