Suppose I have a class, defined something like
class Node {
let label: String
var leftChild: Node?
var rightChild: Node?
func copy() -> Node { /// }
init(label: String, leftChild: Node, rightChildL Node) {
// if left child or right child is uniquely referenced - take ownership
// self.leftChild = leftChild or self.rightChild = rightChild
// otherwise leftChild = leftChild.copy() or rightChild = rightChild.copy()
}
}
isKnownUniquelyReferenced
doesn't work with immutable values, such as function arguments.
Is there an easy way to check whether immutable reference is unique? If not - why not?
(There are workarounds for my specific problem - my question is not about them)
Unique ownership is not really actionable without the ability to take ownership of the unique reference. You can make leftChild
and rightChild
into consuming
arguments, which will make their bindings locally mutable and allow you to transfer unique ownership from the caller to the the new object.
5 Likes
SE-0377 seems to suggest that arguments to initializers are consuming
by default.
Is there documentation that describes the difference between this compiler inferred default consuming and explicit consuming?
To be honest, I didn't notice that initializers are consuming by default (even though they probably should be). Could you point to the exact paragraph that states that?
However, if they aren't consuming by default, should the new code make them consuming unless I strictly must copy the parameter?
init
parameters are indeed passed consuming by default, but you'll only get the locally-mutable binding in the case where it is explicitly made consuming
.
4 Likes
Thanks, excellent explanation.
Is it documented somewhere? I think that SE-0377 doesn't state that explicitly. (At least I couldn't find the appropriate paragraph).
It looks like this section mentions it: swift-evolution/proposals/0377-parameter-ownership-modifiers.md at main · swiftlang/swift-evolution · GitHub
Inside of a function or closure body, consuming
parameters may be mutated, as can the self
parameter of a consuming func
method. These mutations are performed on the value that the function itself took ownership of, and will not be evident in any copies of the value that might still exist in the caller. This makes it easy to take advantage of the uniqueness of values after ownership transfer to do efficient local mutations of the value:
Where does it mention that init
parameters are implicitly consumed?` Sorry if I just have bad reading comprehension skills... 
I've written some stupid code to check this behavior.
For some reason, I've noticed that in order for init
to actually consume the argument, one needs to explicitly pass it as consume argument
. Otherwise, implicit copy is created (unlike in the case of consuming function).
class ConsumedClass {}
class ConsumerClass {
var x: ConsumedClass
consuming func isUniqueOwner() {
if isKnownUniquelyReferenced(&x) {
print("Consumed value is unique")
} else {
print("Consumed value is not unique")
}
}
init(explicitlyConsumed x: consuming ConsumedClass) {
self.x = x
}
init(implicitlyConsumed x: ConsumedClass) {
self.x = x
}
}
func isUnique(_ x: consuming ConsumedClass) {
if isKnownUniquelyReferenced(&x) {
print("Consumed value is unique")
} else {
print("Consumed value is not unique")
}
}
func callIsUnique1(_ x: consuming ConsumedClass) {
isUnique(x)
}
func callIsUnique2(_ x: ConsumedClass) {
isUnique(x)
}
func testConsuming1() {
let x = ConsumedClass()
// x is used again afterwards, x is implicity copied.
// Should print `Consumed value is not unique`
callIsUnique1(x)
// consuming and not used afterwards. no implicit copy.
// Should print `Consumed value is unique``
callIsUnique1(x)
}
func testConsuming2() {
let x = ConsumedClass()
// not consuming. Copied. Should print `Consumed value is not unique`
callIsUnique2(consume x)
}
func testImplicitlyConsumingInit1() {
let x = ConsumedClass()
let y = ConsumerClass(implicitlyConsumed: consume x)
// behaves like consumeIsUnique1 even though the parameter was not declared as consuming
// if implicitly consumed - should print `Consumed value is unique`
// if not the behavior should be similar to callIsUnique2(consume x) and print
// `Consumed value is not unique``
y.isUniqueOwner()
}
func testImplicitlyConsumingInit2() {
let x = ConsumedClass()
// behaves like consumeIsUnique1 even though the parameter was not declared as consuming
// if implicitly consumed - should print `Consumed value is unique`.
// however it prints, `Consumed value is not unique`
let y = ConsumerClass(implicitlyConsumed: x)
y.isUniqueOwner()
}
func testExplicitlyConsumingInit1() {
let x = ConsumedClass()
/// same behavior as with ConsumerClass(implicitlyConsumed: x)
let y = ConsumerClass(explicitlyConsumed: consume x)
y.isUniqueOwner()
}
func testExplicitlyConsumingInit2() {
let x = ConsumedClass()
let y = ConsumerClass(explicitlyConsumed: x)
y.isUniqueOwner()
}
testConsuming1()
testConsuming2()
testImplicitlyConsumingInit1()
testImplicitlyConsumingInit2()
testExplicitlyConsumingInit1()
testExplicitlyConsumingInit2()
my output on debug version is
Consumed value is not unique
Consumed value is unique
Consumed value is not unique
Consumed value is unique
Consumed value is not unique
Consumed value is unique
Consumed value is not unique
and on the release version it is
Consumed value is not unique
Consumed value is unique
Consumed value is unique
Consumed value is unique
Consumed value is unique
Consumed value is unique
Consumed value is unique
Is it expected behavior? I'd expect same code to have the same output both in debug and release builds.
And for some reason, on debug version, an implicit copy is created when calling the initializer, unless I explicitly pass the argument with consume
operator.
It looks like it is only mentioned in passing in SE-0377:
By default Swift chooses which convention to use based on some rules informed by the typical behavior of Swift code: initializers and property setters are more likely to use their parameters to construct or update another value, so it is likely more efficient for them to consume their parameters and forward ownership to the new value they construct. Other functions default to borrowing their parameters, since we have found this to be more efficient in most situations.
SE-0390 makes this more formal in its description of consuming operations:
The following operations are consuming:
[...]
- passing an argument to an
init
parameter that is not explicitly borrowing
:
Debug builds may contain additional copies that get optimized away in release builds.
4 Likes
If functions like append
on array were written today - should have their parameters been made consuming
as well? (after all, they pass ownership of the appended element to the array, and on the other hand if element is used outside, it would be implicitly copied anyway).
If this is the case - should the signature be changed? Or that would be a source/ABI breaking change?