During the first review of SE-0366, we got a lot of feedback asking for more context about where it fits in our broader plans for ownership and performance control. I recently began a pitch for ownership modifiers on function parameters, which fill out our ability to control ownership across function boundaries, and I'd like to re-pitch the take/move operator as part of a collection of mechanisms for controlling ownership and copying within a function body:
- the
take
operator, which explicitly ends the lifetime of a parameter or local variable (which was reviewed asmove
in SE-0366) - the
borrow
operator, which explicitly passes a property or variable by borrow without copying - an attribute for suppressing all implicit copying when working with a parameter or local variable, which I'll call
@noImplicitCopy
to begin with
The take
and borrow
operators provide tools for refining Swift's implicit behavior in common isolated situations, without changing the overall programming model in surrounding code. @noImplicitCopy
, by contrast, fully suppresses implicit copying and requires working with affected values like a move-only type, maximizing control but requiring a more drastic shift in the programming model.
Note that we're not proposing these features to be a replacement for move-only types or for other systems programming features, nor will we stop improving optimization and solidifying the language model to make stronger guarantees about copying behavior. Even if future implementation improvements make these explicit controls less necessary to get optimal behavior, the annotations will still serve a useful purpose as a tool for performance sensitive developers to check their work, and for readers and maintainers of code to understand performance sensitivities in code that they should preserve. We also want to ensure that these features can be deployed to fine-tune sensitive parts of a codebase without virally reshaping the programming model of surrounding Swift code, allowing for other developers to continue being productive even if they aren't familiar with the fine details of ARC and implicit copying.
take
operator
Proposal: SE-0366
it is useful to have an operator that explicitly ends the lifetime of a variable before the end of its scope. This allows the compiler to reliably destroy the value of the variable or transfer ownership of the value at the point of its last use, without depending on optimization and vague ARC optimizer rules. Consider this example:
func test() {
var x: [Int] = getArray()
// x is appended to. After this point, we know that x is unique.
// We want to preserve that property.
x.append(5)
// We create a new variable y so we can write an algorithm
// where we may change the value of y (causing a COW copy
// of the buffer shared with x).
var y = x
longAlgorithmUsing(&y)
consumeFinalY(y)
// We no longer use y after this point. Ideally, x would
// be guaranteed unique so we know we can append again
// without copying.
x.append(7)
}
In the example above, y
's formal lifetime extends to the end of scope. When we go back to using x
, although the compiler may optimize the actual lifetime of y
to release it after its last use, there isn't a strong guarantee that it will. Even if the optimizer does what we want, programmers modifying this code in the future may introduce new references to y
that inadvertently extend its lifetime and break our attempt to keep x
unique. There isn't any indication in the source code that that the end of y
's use is important to the performance characteristics of the code. We can introduce an operator to make that explicit:
func test() {
var x: [Int] = getArray()
// x is appended to. After this point, we know that x is unique.
// We want to preserve that property.
x.append(5)
// We create a new variable y so we can write an algorithm
// where we may change the value of y (causing a COW copy
// of the buffer shared with x).
var y = x
longAlgorithmUsing(&y)
// We no longer use y after this point, so we tell the
// last use to take ownership.
consumeFinalY(take y)
// x will be unique again here.
x.append(7)
}
Note that take y
does not directly correspond to a retain or release of y
. By shortening the lifetime of the value of y
, the compiler is allowed to avoid retains and releases it might otherwise have made to keep a copy of y
available in the caller. The compiler may or may not release the value of y
at the point of take y
, depending on whether the use site is able to take ownership of the value or not.
The take y
operator syntax deliberately mirrors the proposed ownership modifier parameter syntax, (x: take T)
, because the caller-side behavior of the operator is analogous to the callee’s behavior receiving the parameter: the take y
operator forces the caller to give up ownership of the value of x
in the caller, and the take T
parameter will assume ownership of the argument in the callee. They can be used in tandem to forward ownership of a value across call boundaries:
func +(_ a: take String, _ b: String) -> String {
// Transfer ownership of the `self` parameter to a
// mutable variable
var result = take a
// Modify it in-place, taking advantage of
// uniqueness if possible
result += b
return result
}
// Since each result should be uniquely-referenced,
// this append chain will run in linear rather than
// quadratic time
"hello " + "cruel " + "world"
Using take
in a callee’s function parameter declaration doesn’t by itself force the caller to let it take ownership, so this definition of +
can also still be used in normal Swift code, which will still copy values as needed to keep them alive. take
can be used in expressions involving +
to require the ownership forwarding chain to continue:
var foo = "hello "
// Copies `foo` to keep the existing value alive for further access
var bar = foo + "cruel " + "world"
foo += "beautiful " + "world"
// forward ownership of the final `foo` value
var bas = take foo + ", i felt the rain on my shoulder"
borrow
operator
When performing reads of shared mutable state, such as class stored properties, global or static variables, or escaped closure captures, the compiler defaults to copying when it is theoretically possible to borrow. The compiler does this to maintain memory safety and minimize the opportunity for the law of exclusivity to trip, in case other code tries to write to the same objects or global variables while a call is ongoing. For example:
var global = Foo()
func useFoo(x: Foo) {
// We would need exclusive access to `global` to do this:
/*
global = Foo()
*/
}
func callUseFoo() {
// callUseFoo doesn't know whether `useFoo` accesses global,
// so we want to avoid imposing shared access to it for longer
// than necessary. So by default the compiler will
// pass a copy instead, and this:
useFoo(x: global)
// will compile more like:
/*
let copyOfGlobal = copy(global)
useFoo(x: copyOfGlobal)
destroy(copyOfGlobal)
*/
}
Although the compiler is allowed to eliminate the defensive copy inside callUseFoo
if it proves that useFoo
doesn't try to write to the global
variable, it is unlikely to do so in practice. The developer however knows that useFoo
doesn't modify global
, and may want to suppress this copy in the call site. An explicit borrow
operator lets the developer communicate this to the compiler:
var global = Foo()
func useFoo(x: Foo) {
/* global not used here */
}
func callUseFoo() {
// The programmer knows that `useFoo` won't
// touch `global`, so we'd like to pass it without copying
useFoo(x: borrow global)
}
borrow global
suppresses the local copy of global
and passes the reference as-is to useFoo
. If useFoo
did in fact attempt to mutate global
while the caller was borrowing it, the attempt would trigger an exclusivity failure trap at runtime.
It is useful to be able to borrow
parts of an object, such as a reference to an instance property, or a struct field within a property (by contrast with take x
, which passes ownership of an entire value, and therefore only makes sense to apply to variables):
final class A {
var x: B
}
struct B {
var y: String
}
foo(borrow a.x.y) // borrow access to a.x, then borrow .y out of it
When working with local variables, the compiler can statically tell when borrowing in place is safe, so borrow
isn’t strictly necessary. It can still be used as an annotation to guarantee that in-place borrowing occurs, and to have the compiler raise errors in cases where it isn’t possible:
func read(_: borrow String, andModify: inout String) {}
func doStuff() {
var x = "foo"
read(x, andModify: &x) // will copy x to avoid exclusivity error passing the first argument
read(borrow x, andModify: &x) // error: nonexclusive use of &x
}
The borrow expr
syntax intentionally aligns with the proposed ownership modifier syntax for parameters, (x: borrow T)
, because the caller-side behavior of borrow expr
is analogous to the callee’s handling of the parameter value. A borrow T
parameter declares that the callee will borrow the value it is passed from the caller, but does not by itself prevent callers from using a copy as the passed value. Using borrow expr
at the call site forces the caller to pass the borrowed argument in-place.
@noImplicitCopy
attribute
There are many circumstances when it’s desirable to suppress implicit copying altogether, and treat particular values as being move-only without making their type completely uncopyable. We could provide an attribute that can be applied to function parameters and variables to mark that variable as not being copyable:
func update(@noImplicitCopy array: inout [String]) {
array.append("hello")
array.append("world")
let x = array // error: forces a copy of `array`
array.append("oh no")
}
If a type is copyable, but copying the type is expensive or otherwise best avoided (because it’s large, contains a lot of reference-counted fields, involves C++ copy constructors, etc.), it could be useful to tag a type as no-implicit-copy, to make all variables that are (concretely) of the type no-implicit-copy:
@noImplicitCopy struct Gigantic {
var fee, fie, fo, fum: String
}
func operate(on value: Gigantic) {
var newValue = value
newValue.fee += newValue.fie
operate(on: value, and: newValue) // error: forces a copy of `value`
}
Or you might want to suppress copies generally within a scope, such as a hot loop:
for item in items {
@noImplicitCopy do {
...
}
}
Note that, unlike true move-only types, all of these would be local, non-transitive restrictions. A no-implicit-copy variable can still be passed to another function that may copy it:
func foo(@noImplicitCopy x: String) {
bar(x: x)
}
func bar(x: String) {
// OK to copy `x` in `bar`
var y = x
y += "bar"
print(x, y)
}
And concrete types marked as no-implicit-copy would be implicitly copyable when used as generic or existential types (unless those generic values are also marked no-implicit-copy):
func duplicate<T>(_ value: T) -> (T, T) {
// OK to copy value in a generic context
return (value, value)
}
duplicate(Gigantic()) // this call is OK
This allows developers to apply @noImplicitCopy
locally in their own code without cutting themselves off from the greater Swift ecosystem, which would need to retrofit annotations in order to support a truly transitive “move-only value” constraint, or imposing transitive constraints on their clients. Move-only types will be the language tool for transitively preventing copies.
Because the language semantics can’t copy a no-implicit-copy variable, such variables have “eager move” lifetime semantics, meaning their lifetime ends after an operation takes ownership of their value, regardless of whether the take
operator is used:
func consume(_: take String) {}
func doStuff() {
do {
var x = "hello"
x += " world"
consume(x)
print(x) // This is fine, we'll copy `x` to prolong the local var's lifetime
}
do {
@noImplicitCopy var x = "hello"
x += " world"
consume(x)
print(x) // ERROR: x used after take. we're not allowed to copy it
}
}
No-implicit-copy values are also always borrowed in-place when used as borrowed arguments. So certain formulations may trigger static or dynamic exclusivity errors that would be accepted quietly for copyable values:
func read(_: borrow String, andModify _: inout String) {}
func doStuff() {
do {
var x = "hello"
x += " world"
// This is fine, we'll copy `x` to sidestep the exclusivity error that'd arise
// from trying to simultaneously pass x by inout and by borrow
read(x, andModify: &x)
}
do {
@noImplicitCopy var x = "hello"
x += " world"
// ERROR: attempt to exclusively access x during a borrow
read(x, andModify: &x)
}
}
Values of move-only types would always have these “eager move” lifetime semantics as well.
Explicit copy
operation
Without the ability to copy implicitly, an obvious question is, how do we explicitly copy? Because @noImplicitCopy
isn’t transitive, it is easy enough to write a function that copies on behalf of its caller:
func copy<T>(_ value: borrow T) -> T { return value }
func doStuff() {
@noImplicitCopy var x = "hello"
x += " world"
read(copy(x), andModify: &x)
}
But by analogy to take x
and borrow x
, we may want to make copy
also be a contextual operator:
func doStuff() {
@noImplicitCopy var x = "hello"
x += " world"
read(copy x, andModify: &x)
}