We start with a container whose element may be either Copyable or Escapable, which will be determined by the calling context:
struct Container<Element: ~Escapable, ~Copyable>: ~Escapable, ~Copyable {
let _element: Element
}
Accessing the container's element in a Copyable context gives you an independent element value before allowing you to use that element value in an expression.
func foo<T>(_: borrowing T)
func copyableContext<T: Copyable>(...) {
var container: Container<T> = ...
// container may be mutably captured
// foo gets a copy of the element
foo(/*copy*/ container._element)
}
This allows for a minimal exclusivity constraints on the container. It is equivalent to:
func copyableContext<T: Copyable>(...) {
var container: Container<T> = ...
// exclusive access to 'container' begins here
let temp = container._element
// exclusive access to 'container' ends here
foo(temp)
}
The same Copyable semantics also apply to computed property getters and read accessors:
extension Container where Element: ~Escapable & Copyable {
var independentElement {
get { _element }
}
var scopedElement {
read { yield _element }
}
}
{
foo(/*copy*/ container.independentElement)
foo(/*copy*/ container.scopedElement)
}
In a ~Copyable
context, the same property read requires the both the container its property's value to be borrowed for the duration of the read. That prolongs the container's exclusivity scope:
func foo<T>(_: borrowing T)
func noncopyableContext<T: ~Copyable>(...) {
var container: Container<T> = ...
// exclusive access to 'container' begins here
foo(/*borrow*/ container._element)
// exclusive access to 'container' ends here
}
In a ~Copyable
context, a read accessor also borrows the container, and prolongs its exclusivity, just like a stored property:
// exclusive access to 'container' begins here
foo(/*borrow*/ container.scopedElement)
// scopedElement coroutine cleanup runs here
// exclusive access to 'container' ends here
This is necessary to borrow the element for the duraton of the call to foo
.
In an ~Escapable
context, the same property read also prolongs the container's exclusivity scope despite copying the element:
func nonescapableContext<T: ~Escapable>(...) {
var container: Container<T> = ...
// exclusive access to 'container' begins here
foo(/*copy*/ container._element)
// exclusive access to 'container' ends here
}
This is necessary to keep the container alive and read-only for the duraton of the call to foo
.
In a ~Escapable
context, a getter or read accessor behaves the same as a stored property:
// exclusive access to 'container' begins here
foo(/*copy*/ container.independentElement)
// exclusive access to 'container' ends here
// exclusive access to 'container' begins here
foo(/*copy*/ container.scopedElement)
// 'scopedElement' coroutine ends here
// exclusive access to 'container' ends here
Although computed property read accessors have the same exclusivity and ownership as stored properties, they differ in their projection semantics:
struct NE: ~Escapable {
struct NC: ~Copyable {
func ne() -> NE
}
A stored property projection is valid for the lifetime of its container. It is not confined to a local scope:
func foo<T>(_: borrowing T)
func NCContext(...) {
var container: Container<NC> = ...
let projectedNE: NE
do {
projectedNE = container._element.ne()
}
foo(projectedNE) // OK: extends exclusive access on 'container'
}
The call to ne()
returns a value that depends on the container._element
projection. Because the compiler can see the projection, it can prolong the container's exclusive access across the 'do' block.
Read accessors, on the other hand, are always confined to their local scope:
func NCContext(...) {
var container: Container<NC> = ...
let scopedNE: NE
do {
scopedNE = container.scopedElement.ne()
// 'scopedElement' coroutine ends here
// exclusive access to 'container' ends here
}
foo(scopedNE) // ERROR: no exclusive access on 'container'
}
This is a natural consequence of coroutines, which require a cleanup operation to execute at the end of a lexical scope.
A borrow
accessor has the same projection semantics as a stored property. Unlike a read accessor, it is not confined to a local scope:
func foo<T>(_: borrowing T)
extension Container where Element: ~Escapable & ~Copyable {
var borrowedElement {
borrow { _element }
}
}
struct NE: ~Escapable {
struct NC: ~Copyable {
func ne() -> NE
}
func NCContext(...) {
var container: Container<NC> = ...
let borrowedNE: NE
do {
borrowedNE = container.borrowedElement.ne()
}
foo(borrowedNE) // OK: extends exclusive access on 'container'
Projection semantics allows function composition:
func NCContext(container: Container<NC>) -> NE {
return container.borrowedElement.ne() // OK: caller has exclusive access to 'container'
}
In a Copyable & Escapable context, a borrow
accessor produces an independent value without prolonging exclusive access on the container:
func foo<T>(_: borrowing T)
extension Container where Element: ~Escapable & ~Copyable {
var borrowedElement {
borrow { _element }
}
}
func copyableContext<T: Copyable>(...) {
var container: Container<T> = ...
// container may be mutably captured
// foo gets a "defensive copy" of the element
foo(/*copy*/ container.borrowedElement)
}
This is desirable because it allows borrow accessors to be source compatible with all the other immutable reads: stored properties, getters, and read accessors.
In a Copyable context, avoiding copies requires extra effort. The programmer can use a borrow
operator to prevent defensive copies. This forces the container's exclusive access to be prolonged:
func foo<T>(_: borrowing T)
func copyableContext<T: Copyable>(...) {
var container: Container<T> = ...
// exclusive access to 'container' begins here
foo(borrow container._element)
// exclusive access to 'container' ends here
}
Some properties may want to borrow by default even in Copyable contexts. This requiers a new @dependentBorrow
annotation. When a dependent-borrow property is used in an expression that expects a borrowed value, it is equivalent to writing borrow
operator in front of the property:
struct Container<Element: ~Escapable, ~Copyable>: ~Escapable, ~Copyable {
@dependentBorrow
let _dependentElement: Element
}
func copyableContext<T: Copyable>(...) {
var container: Container<T> = ...
// exclusive access to 'container' begins here
foo(/*borrow*/ container._dependentElement)
// exclusive access to 'container' ends here
}
A dependent-borrow property gives programmers a guarantee that the compiler won't introduce "hidden" copies in the calling context in order to produce an independent value for the property:
protocol HasBar {
borrowing func bar()
}
func HasBarContext(...) {
var container: Container<HasBar> = ...
container._dependentElement.bar() // no copy
}
The @dependentBorrow
annotation makes such an access to the property behave as if it were written:
(borrow container._dependentElement).bar()
Using a dependent-borrow in an explicit assignment or as a consuming argument still generates a copy because these uses require an independent copy of the element:
func take<T>(_: consuming T)
{
let elt = /*copy*/ container._dependentElement
take(/*copy*/ container._dependentElement)
}
This convenience of forcing a borrow in a borrowing expression comes at the expense of source compatibility with regular immutable properties; in a Copyable context, a @dependentBorrow
property is not source compatible with a regular property in either direction. Adding or removing the annotation breaks library evolution.
@dependentBorrow
only affects the property semantics in a Copyable context. When a dependent-borrow property is used in a ~Copyable
or ~Escapable
context, the access produces the same dependent borrow with or without the @dependentBorrow
annotation.
@dependentBorrow
can apply to any immutable property that can produces a borrowed value: stored property, read
, and borrow
:
extension Container where Element: ~Escapable & ~Copyable {
@dependentBorrow
var scopedDependentElement {
read { yield _element }
}
@dependentBorrow
var borrowedDependentElement {
borrow { _element }
}
}
A get
requirement may be witnessed by: stored property, get
, read
, borrow
, unsafeAddress
.
A read
requirement may be witnessed by: stored property, get
, read
, borrow
, unsafeAddress
.
A borrow
requirement may only be witnessed by: stored property, borrow
, unsafeAddress
.
In addition to the requirements above, a @dependentBorrow
requirement may only be witnessed by a @dependentBorrow
property, and a @dependentBorrow
property may only witness a @dependentBorrow
requirement.
The mutable analog to borrow
accessors are mutate
accessors:
struct Container<Element: ~Escapable, ~Copyable> {
private let _element: Element
var element: Element {
borrow {
_element
}
mutate {
&_element
}
}
}
Mutable access requires exclusivity for the duration of the access. Existing stored property semantics already force an exclusivity scope for that duration. We therefore don't need a @dependentBorrow
annotation, and don't have any source compatibility issues to contend with.
C++ APIs that return references, such as vector[i]
and shared_ptr.pointee
will be imported with two additional attributes that affect their semantics:
-
@dependentBorrow
-
@noImplicitCopy
As in:
struct shared_ptr<Pointee> {
@dependentBorrow
@noImplicitCopy
var pointee: Pointee {
borrow { /*imported*/ }
}
}
@dependentBorrow
ensures that code can round-trip the returned reference, passing it immediately back to a C++ method without potentially inducing copy:
vector[i].foo() // element is not copied
shared_ptr.pointee.foo() // pointee is not copied
This is important for correctness because the vector element or pointee may be a base class, which leads to undefined behavior when copied.
@noImplicitCopy
provides an extra level of safety by catching assignments
let elt1 = vector[i] // ERROR: requires an explicit 'copy'
let elt2 = copy vector[i] // No error, but may be undefined.
elt2.foo()
In the future, we will support a borrow
binding, which can bind a variable name to dependent borrow:
borrow elt = vector[i] // OK
elt.foo() // No copy of 'elt'
Alternatively, we can import all C++ references as a new nonescapable Swift Ref<T>
type:
@lifetime(pointee) // initialization requires a lifetime dependence
struct Ref<T: ~Copyable, ~Escapable>: ~Copyable, ~Escapable {
let pointer: UnsafePointer<T>
@dependentBorrow
var pointee { unsafeAddress { pointer } }
}
This would allow the use of local variables without a borrow
binding. It would also eliminate the need for a @noImplicitCopy
property convention. The downside it that dereferencing the value would require an extra pointee
access, and assigning that pointee into a variable could still be undefined:
let elt = vector[i] // OK
elt.pointee.foo() // No copy of 'pointee'
let val = elt.pointee // potentially undefined