consume operator to end the lifetime of a variable binding
01 何が問題だったのか
Swiftは参照カウントとcopy-on-write(COW)によって値セマンティクスを実現しており、通常は開発者がパフォーマンスやメモリ管理を細かく気にする必要はありません。しかしパフォーマンスに敏感なコードでは、COWデータ構造の一意性を保ちたい、retain/releaseの呼び出しを減らしたい、といった要求が出てきます。
たとえば次のように Array を構築して別の関数に渡し、そのあと別の値で置き換えるコードを考えます。
func test() {
var x: [Int] = getArray()
x.append(5) // この時点で x のバッファは一意
doStuffUniquely(with: x)
x = [] // 以降は古い値を使わない
doMoreStuff(with: &x)
}
func doStuffUniquely(with value: [Int]) {
var newValue = value
newValue.append(42)
process(newValue)
}
この x は doStuffUniquely(with:) に渡したあと再代入されるだけで、古い値は二度と使いません。したがって理想的には、x の所有権(ownership)をそのまま doStuffUniquely に forward してコピーなしで渡し、さらに受け取った側も value を newValue にforwardして、一意なバッファのまま破壊的変更を行いたいところです。
コンパイラは条件が揃えばこうした最適化を自動的に行えますが、複数の解析が噛み合う必要があり、実際に効いているかは書き方に依存します。開発者としては、「この値はここで使い終わり」「以降は使わない」という意図を明示して最適化を保証させ、あとから誰かがコードを変更してその前提を壊したときにはコンパイルエラーで気づけるようにしたい、という要求があります。
また、Swiftには「local変数のライフタイムをスコープで短くする」ことは以前から可能でしたが、スコープだけに頼るとネストが深くなり、ライフタイムが交互に重なる場合や、関数パラメータ・inout パラメータのライフタイムを途中で終わらせたい場合には対応できません。
02 どのように解決されるのか
新しく consume 演算子を導入し、ローカルの let / var や関数パラメータといった 静的なライフタイムを持つbinding の現在の値をその場で消費(consume)してライフタイムを終わらせられるようにします。consume x はランタイム上は単に x の現在の値に評価されますが、コンパイル時にはその時点で x の所有権がbindingから転送されたものとして扱われ、以降で x を参照するコードはエラーになります。
func test() {
var x: [Int] = getArray()
x.append(5)
doStuffUniquely(with: consume x) // ここで x のライフタイム終了
x = [] // var なので再代入すればまた使える
doMoreStuff(with: &x)
}
受け側でも、consuming パラメータとして受け取った値をさらに別のローカル変数に consume でforwardすることで、コピーを挟まずに一意なバッファを引き継いで変更できます。
func doStuffUniquely(with value: consuming [Int]) {
var newValue = consume value // value からバッファを奪って newValue に引き継ぐ
newValue.append(42)
process(newValue)
}
あとから保守でうっかり consume のあとに元のbindingを使うコードを足してしまっても、コンパイラが検出してくれます。
func test() {
var x: [Int] = getArray()
x.append(5)
doStuffUniquely(with: consume x)
// ERROR: x used after being consumed
doStuffInvalidly(with: x)
x = []
doMoreStuff(with: &x)
}
consume できるbinding
consume のオペランドは 静的なライフタイムを持つbinding への参照でなければなりません。具体的には次のいずれかです。
- 直接囲んでいる関数のローカル
let - 直接囲んでいる関数のローカル
var - 直接囲んでいる関数のパラメータ
mutatingまたは__consumingメソッドのself
さらに、そのbindingが次のいずれかに当てはまる場合は consume できません。
@escapingクロージャやネストした関数にキャプチャされている- property wrapperが適用されている
get/set/didSet/willSet/_read/_modifyなどのアクセサが付いているasync letである
computed propertyや格納プロパティへの consume は将来の拡張として残されています(後述)。
bindingに対する操作であること
consume は値ではなくbindingに対して働きます。同じ値を別のbindingに束ねておけば、元のbindingを consume したあとでもそちらは使い続けられます。
func f() {
let x: SomeClassType = ...
useX(x)
let other = x // other は x のライフタイムを延長する新しいbinding
_ = consume x // x のライフタイムはここで終了
useX(other) // other はまだ使える
useX(other) // other もまだ使える
}
もちろん other のほうも独立に consume でき、それぞれのbindingに対して個別に診断が行われます。
フロー依存の解析と var の再初期化
解析はフロー依存です。条件分岐の一方だけで consume した場合、その分岐の後ろでだけ使えなくなります。
if condition {
let y = consume x
useX(x) // ERROR: Use after consume
} else {
useX(x) // OK
}
useX(x) // ERROR: 両方のパスで x が生きているとは限らない
var の場合は、consume したあとに再代入すれば再び使えるようになります。
if condition {
_ = consume x
useX(x) // ERROR
x = newValue // 再初期化
useX(x) // OK
} else {
useX(x) // OK(このパスでは consume していない)
}
useX(x) // OK(if で再初期化済み、else では consume していない)
inout パラメータ
inout パラメータも consume できます。解析は var と同様ですが、関数から戻るとき(return でも throw でも)にはパラメータに値が入っていなければならないため、consume したあとは関数を抜ける前に必ず再代入する必要があります。
func f(_ buffer: inout Buffer) { // error: 'buffer' not reinitialized after consume!
let b = consume buffer
b.deinitialize()
// ... buffer に再代入せずに return してしまうとエラー
}
defer を使って、早期リターンや throw を含むすべての経路で再初期化するのが定石です。
func f(_ buffer: inout Buffer) {
let b = consume buffer
defer { buffer = getNewInstance() }
try b.deinitializeOrError()
// ...
}
戻り値を捨てるときは _ = を明示する
consume はbindingのライフタイムを終わらせるだけでなく、現在の値を評価結果として返します。戻り値を使わずに consume x と書くと、戻り値のある関数呼び出しを捨てたときと同様に警告になります。値を単に破棄したいときは _ = consume x と明示的に書きます。
構文上の注意
consume は文脈依存のキーワードです。consume という名前の関数やプロパティと衝突しないよう、オペランドは識別子から始まる必要があり、識別子またはpostfix式に限定されます。
consume x // OK
consume [1, 2, 3] // consume という property への subscript アクセスとして解釈
consume (x) // consume という関数の呼び出しとして解釈
consume x.y.z // 構文上はOK
consume x[0] // 構文上はOK
consume x + y // (consume x) + y として解釈
Future directions
以下は今回のスコープ外で、今後の検討対象として言及されています(実現を約束するものではありません)。
- エスケープしたローカル変数やクラスの格納プロパティなど、動的なライフタイムを持つbindingに対する
consume(ランタイムチェックが必要になるため、別構文にする可能性があります)。 @frozenなstructやタプルに対して、フィールドごとに独立にconsumeする拡張。- computed propertyやproperty wrapperに対する
consume(”consuming getter” のような新しいアクセサの導入が必要になる可能性があります)。 borrow演算子や@noImplicitCopy属性など、implicit copyを抑制する一連の機能との連携(consumeはその一部に位置づけられています)。- パラメータ側で受け取り方を明示する
borrow/consumeownership modifier(func foo(x: consuming T)など)との組み合わせ。