Noncopyable Generics in Swift: A Code Walkthrough

It isn’t copied, it is consumed, just like the init consumed it.

Note that copies here are semantic. It does not mean bits themselves are etched into a particular location in memory and remain there forever.

6 Likes

Right, this must be an initialize(to: consuming T). My forum search isn’t finding where that was discussed, though I do recall it being at least mentioned.

This raises a further question: does the compiler ever have to emit two versions of a function that differ in how they handle whether the concrete value is Copyable or not? (Apologies if this too has been addressed elsewhere.)

1 Like

The documentation does not reflect this, and I suppose the compiler has some hard-coded behavior (initializer arguments are special as well).

It would greatly help if the various documentations would be updated to reflect this (except for initializers), now that consuming, borrowing, etc are settled and public. It's easier to reason with an accurate reference!

4 Likes

That's the whole point of function overloads. Reading the code in a blog these days is not enough to fully understand, we must use the right toolchains + editor + code completion etc. in order to fully understand the different overloads.

2 Likes

Overloads are usually documented. I’m just guessing that the docs haven’t been updated yet.

This is an in-development feature still going through the evolution process, never mind not released yet. So there should be no expectation that docs would be updated, in fact it would be inappropriate for them to be updated.

As @Datagram points out though, you can see the method signature with a nightly toolchain. Part of adopting Pointee: ~Copyable on UnsafeMutablePointer involved spelling out when a whenever a Pointee: ~Copyable was being consumed or borrowed, because ~Copyable things require that (unlike copyable parameters, which default to borrow for regular functions, but consume for inits).

In this case consuming is the right answer because it is initializer-like, much like List.append(_: consuming Element).

5 Likes

Yeah, so the bad news is that the UnsafeMutablePointer.initialize(to:) was incorrectly taking it's argument as __shared (the default for arguments outside of initializers which is why documentation doesn't reflect anything). As part of the standard library adoption, we need to deprecate this entry point because it is complete bogus and have to replace it with a new one that properly takes its argument as consuming. We will have a proposal outlining all of the work in the standard library needed to support noncopyable generics cc: @lorentey

6 Likes

The documentation doesn't show _shared anyway, it hides underscored modifiers since these are implementation details.

We should note that this deprecation will be transparent to users (other than that some uses of the function might get more efficient once it acquires the right calling convention). Borrowing/consuming of copyable types is just handled automatically by the compiler. And since there was no support for generic non-copyable types when they were introduced, those couldn't previously be stored in a pointer.

1 Like

Well the API didn't mark it as __shared, just noting that that is the default and is the current convention of the argument.

I.e. we're going from:

public func initialize(to: Pointee)

to

public func initialize(to: consuming Pointee)
3 Likes

Right, it is that it should have been annotated __owned like e.g. Array.insert was. But either way, you wouldn't see that come through in the docs. Whereas once it gets marked up for official consuming use, you will.

7 Likes

Is this perhaps a motivating case for the ability to constrain to non-copyable (as opposed to maybe-copyable) types? Here the standard library is forced to deprecate UnsafeMutablePointer<T: Copyable>.initialize(to: T) in favor of UnsafeMutablePointer<T: ~Copyable>(to: consuming T). If we could instead constrain to non-copyable types, this solved with a purely additive change by adding a new overload for where T: NonCopyable.

It may be worth it for the standard library to deprecate and replace the old method, but it has the advantage of coming with the compiler, which can add any hacks necessary to prefer the T: ~Copyable version over the usually-more-specific T: Copyable version.

1 Like

I don't think so. Like I said, "deprecated" is the wrong word. initialize(to:) was essentially mis-declared and needs replacing, including for copyable types which also ought to be consumed into it. The only difference with copyable types is that they can work with the mis-declared method. There is no win here from creating a second method.

But even if that was what was done, and we couldn't "swap out" the old one for the new one (which we can, we have that technology), there is no benefit from constraining Box to only non-copyable types because of that that I can see.

3 Likes

You wouldn’t even need to do that. They have different protocol constraints, and Copyable is more constrained than ~Copyable, so it won’t be ambiguous. That’s not the resolution we want (calling initialize(to:) always needs to consume a value, whether or not it has to copy to get a consumable value), but it is possible.

2 Likes

But if all you did was add a where Pointee: ~Copyable version, the existing non-consuming method would be called for the vast majority of types.

1 Like

You’re right that it’s difficult to come up with a good use case for constraining a whole type to NonCopybale, but I’m thinking specifically about constraining certain extensions to NonCopyable, for retroactively fixing a type to work with move-only types without breaking existing clients.

Putting aside that changing this does not break existing clients... let's say that there is a case where it is preferable to keep a function's parameter as borrowing, and so the decision was to add a second overload where Parameter: ~Copyable. How does the ability to constrain a generic placeholder to forbid copyable types help in that case?

BTW here's another example to use while thinking about this stuff. The min method from the standard library is declared as:

func min<T: Comparable>(_ x: T, _ y: T) -> T

x and y are taken as the default convention of borrowing, but a copy of one of them is returned. So this can never be made to work with non-copyable types. But it is perhaps desirable to take them borrowed, since only one of them needs to be copied. If both were taken consuming, then unless this was the last ever use of both arguments (which is unlikely thinking about most uses of min) then both of them would need to be copied. To work with non-copyable types OTOH, you probably instead a version that borrows both arguments and returns an enum indicating which one was smaller. That one would be used in e.g. a sort function that works on collections of maybe-copyable things.

Still, with this example, I'm still not seeing where "only allow actually non-copyable types" helps.

4 Likes

This is spelled < ;-)

5 Likes

Ok fine, pretend I’d bothered to spell out the full variadic version :) though I guess that’s not especially useful.

*shrug* No, point taken, finding the minimum element of a collection is still useful (and should return an Index).

2 Likes

I realize I’m getting increasingly diminishing returns from this example but there’s still a distinction between the minimum of two or more arguments, where there has to be a minimum, vs a collection that needs to return an optional index (though don’t talk too loudly about that or the non-empty collection enthusiasts will hear us)

5 Likes