Swift Digest
SE-0453 | Swift Evolution

固定サイズ配列InlineArray

InlineArray, a fixed-size array

Proposal
SE-0453
Authors
Alejandro Alonso
Review Manager
Freddy Kellison-Linn
Status
Implemented (Swift 6.2)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swift で「順序付きの同じ型の要素の並び」を扱いたくなったとき、これまでの第一の選択肢は Array でした。Array は非常に汎用的で使いやすい一方、常に ヒープ割り当て される可変長コンテナであり、要素数が静的にわかっている場面では明らかに過剰です。ヒープ確保や参照カウントのコストは、組み込み領域のようにメモリや割り当て回数が制約される環境、あるいはパフォーマンスクリティカルなコードでは無視できないオーバーヘッドになります。

スタック上に並べたいだけならタプルしかなかった

N 個の値をスタック上にまとめて置きたいだけなら、これまではタプルで代用できました。

func complexAlgorithm() {
  let elements = (first, second, third, fourth)
}

ただしタプルは 動的な添字アクセスイテレーション もできません。

func complexAlgorithm() {
  let elements = (first, second, third, fourth)

  for i in 0 ..< 4 {
    // error: cannot access element using subscript for tuple type
    //        '(Int, Int, Int, Int)'; use '.' notation instead
    compute(elements[i])
  }
}

withUnsafeTemporaryAllocation では unsafe に落ちてしまう

SE-0322 の withUnsafeTemporaryAllocation を使えば、スタック(または必要に応じてヒープ)に確保した領域に対して UnsafeMutableBufferPointer 経由で添字アクセスやイテレーションができます。しかし、これは名前の通り unsafe API であり、安全に使うためには寿命や初期化状態の管理をすべて自分で負う必要があります。メモリ安全性を掲げる Swift において、「サイズが静的に決まっている配列をスタックに置きたい」だけのコードがこの層まで降りなければならないのは不健全です。

noncopyable 要素をインラインに並べる手段が無い

さらに、AtomicMutex のような noncopyable な型をそのまま並べた固定サイズの配列は、Array でもタプルでも表現できません。ヒープに逃がさず、コピーもせず、N 個の noncopyable 値を所有する「箱」を作るには、言語・ライブラリ側にそれ専用の型が必要です。

02 どのように解決されるのか

標準ライブラリに、要素数を型パラメータとして持つ固定サイズの配列型 InlineArray を追加します。C の T[N]、C++ の std::array<T, N>、Rust の [T; N] に相当する型で、SE-0452 で導入された整数ジェネリックパラメータを用いて次のように定義されます。

public struct InlineArray<let count: Int, Element: ~Copyable>: ~Copyable {}

extension InlineArray: Copyable where Element: Copyable {}
extension InlineArray: BitwiseCopyable where Element: BitwiseCopyable {}
extension InlineArray: Sendable where Element: Sendable {}

要素が noncopyable である場合にも対応するため、InlineArray 自身も ~Copyable として宣言され、要素がコピー可能なときだけ条件付きで Copyable に適合します。

Proposal 本文中ではレビュー過程での仮名として Vector という表記が残っていますが、受理時に InlineArray にリネームされたため、ここでは一貫して InlineArray と表記します。

インライン確保でヒープ割り当てが発生しない

InlineArray のストレージは「その値が置かれる場所」に直接インライン確保されます。ローカル変数ならスタック、クラスのプロパティなら他のプロパティと一緒にそのインスタンスのヒープ領域の中、という具合です。ストレージのためだけに暗黙のヒープ割り当てが発生することは一切ありません。

func complexAlgorithm() {
  // これはスタック上の割り当て。malloc も参照カウントも発生しない
  let elements: InlineArray<4, Int> = [1, 2, 3, 4]

  for i in elements.indices {
    compute(elements[i]) // OK
  }
}

配列リテラルによる初期化

InlineArrayExpressibleByArrayLiteral には適合しません。そのプロトコルは一度 Array の値を経由するため、スタック上の InlineArray を初期化するためにわざわざ Array を確保するのは本末転倒だからです。そこで、配列リテラルから InlineArray を作るケースだけコンパイラが特別扱いし、各要素をそのスタックスロット上で直接初期化します。中間的な Array 値が作られたり、要素がコピー・ムーブされたりすることはありません。

let numbers: InlineArray<3, Int> = [1, 2, 3]

配列リテラルから InlineArray が生成されるのは、コンテキストから具体的に InlineArray 型だと分かっているときだけです。既存の Array リテラル解決ルールには影響せず、ソースコード互換性も保たれます。

let a = [1, 2, 3]                        // Swift.Array
let b: InlineArray<3, Int> = [1, 2, 3]   // Swift.InlineArray

func generic<T>(_: T) {}
generic([1, 2, 3])                        // Array として渡る
generic([1, 2, 3] as InlineArray<3, Int>) // InlineArray として渡る

要素数・要素型はいずれも推論可能で、どちらも省略すればリテラル側から両方が決まります。

let a: InlineArray<_, Int> = [1, 2, 3] // InlineArray<3, Int>
let b: InlineArray<3, _> = [1, 2, 3]   // InlineArray<3, Int>
let c: InlineArray = [1, 2, 3]         // InlineArray<3, Int>

func takesGenericInlineArray<let N: Int>(_: InlineArray<N, Int>) {}
takesGenericInlineArray([1, 2, 3]) // N は 3 と推論される

リテラルの要素数が型の count と一致しない場合はコンパイルエラーになります。

// error: expected '2' elements in InlineArray literal, but got '3'
let x: InlineArray<2, Int> = [1, 2, 3]

クロージャベースの初期化

リテラル以外にも、インデックスや直前の要素からクロージャで各要素を生成する初期化子が用意されており、noncopyable な要素にも対応します。

extension InlineArray where Element: ~Copyable {
  // インデックスを受け取り、その位置に置く値を返すクロージャで初期化
  public init<E: Error>(_ next: (Int) throws(E) -> Element) throws(E)

  // 先頭の値と、「直前の要素の借用」から次の要素を作るクロージャで初期化
  public init<E: Error>(
    first: consuming Element,
    next: (borrowing Element) throws(E) -> Element
  ) throws(E)
}

extension InlineArray where Element: Copyable {
  // 同じ値のコピーで全要素を埋める
  public init(repeating: Element)
}

具体的には次のように使えます。

// [Atomic(0), Atomic(1), Atomic(2), Atomic(3)]
let incrementingAtomics = InlineArray<4, Atomic<Int>> { i in
  Atomic(i)
}

// 先頭 Sprite() と、それを順にコピーして作った計 4 要素
let copiedSprites = InlineArray<4, _>(first: Sprite()) { $0.copy() }

// InlineArray<3, Mutex<Int>> として推論される
let literalMutexes: InlineArray = [Mutex(0), Mutex(1), Mutex(2)]

クロージャの途中で throw された場合、すでに初期化済みの要素は順にデイニシャライズされたうえでエラーが呼び出し側に伝播します。値が使われなくなった時点で、各要素は通常通り要素ごとにデイニシャライズされます。

メモリレイアウト

InlineArray<count, Element> のサイズ・ストライドは Element の stride を count 倍した値、アラインメントは Element のものと等しくなります。タプルと違ってパディングが挟まらず、Element の stride そのものが間隔になる点が重要です。

MemoryLayout<UInt8>.stride == 1
MemoryLayout<InlineArray<4, UInt8>>.size == 4
MemoryLayout<InlineArray<4, UInt8>>.stride == 4
MemoryLayout<InlineArray<4, UInt8>>.alignment == 1

コレクション的な API

InlineArraySequenceCollection には 適合しません(理由は後述)。ただし、コレクションとして日常的に欲しい API は直接定義されています。

extension InlineArray where Element: ~Copyable {
  public typealias Element = Element
  public typealias Index = Int

  // インスタンスを介さずに静的にも取れる count
  public static var count: Int { count }

  public var count: Int { count }
  public var indices: Range<Int> { 0 ..< count }
  public var isEmpty: Bool { count == 0 }
  public var startIndex: Int { 0 }
  public var endIndex: Int { count }

  public borrowing func index(after i: Int) -> Int
  public borrowing func index(before i: Int) -> Int

  public mutating func swapAt(_ i: Int, _ j: Int)

  public subscript(_ index: Int) -> Element
  public subscript(unchecked index: Int) -> Element // 境界チェック無し
}

noncopyable 要素を持つ InlineArray も、indices を介したインデックスベースのループで走査できます。

let atomicInts: InlineArray<3, Atomic<Int>> = [Atomic(1), Atomic(2), Atomic(3)]

for i in atomicInts.indices {
  print(atomicInts[i].load(ordering: .relaxed))
}

同名の既存型との共存

InlineArray という名前はユーザ側で定義されていることもあり得ますが、標準ライブラリ側は「ユーザ定義型を優先する」シャドーイングルールを持っており、モジュールに InlineArray が定義されていればそちらが常に優先されます。ジェネリック引数の数が合わなくてもユーザ側の定義が選ばれ、明示的に import していないファイルでだけ Swift.InlineArray が見えます。既存プロジェクトへのソース影響は起こりません。

名前について

当初 Vector として提案されましたが、数学での Vector は固定サイズのベクトルを指すのに対し、C++ の std::vector のように「可変長」を指す用法が広まっているなど、語義の混乱が避けられません。レビューを経て、固定サイズ・インライン確保という特徴を素直に表す InlineArray に改名されました。Swift.ArraySwift.InlineArray の対比は、C++ の std::vector / std::array とは対応関係が逆になる点に注意してください。

03 今後の見通し

今回の Proposal で導入されるのは InlineArray のコア型に限られており、周辺機能はいずれも今後の検討対象として挙げられているだけです。以下はあくまで方向性として示されているもので、実現を約束するものではありません。

Equatable / Hashable / CustomStringConvertible などへの適合

要素が Equatable などに適合するときの条件付き適合として InlineArray 側にもこれらを追加することは可能ですが、InlineArray は noncopyable な要素を持てる一方で Equatable などのプロトコル自身はまだ noncopyable 一般化が済んでいません。先に「コピー可能な要素のとき限定」で適合を入れてしまうと、後から noncopyable 要素にも対応させたときに availability の取り扱いが複雑になるため、プロトコル側の一般化を待ってから検討する方針です。

Sequence / Collection への適合

Array と異なり InlineArray は copy-on-write を持たず、値は eager にコピーされます。この状態で Sequence / Collection に適合させてしまうと、汎用アルゴリズムや slicing 経由で暗黙のコピーが多発しかねません。将来的には noncopyable コンテナにも対応した新しい Collection 系プロトコルを別途提案する見通しで、InlineArray の適合はそちらが固まってから検討されます。

ただし、count / indices / subscript といった「コレクションとして日常的に必要になる API」は InlineArray 自身に直接定義されているため、新プロトコルを待たずにインデックスベースの走査などはできます。

Span 系 API

SE-0447 で導入された SpanInlineArray から取得するための API として、withSpan のようなクロージャ版や、lifetime 依存の computed property の追加が考えられます。ただし、後者には lifetime アノテーションの言語機能が必要で、関連する Proposal が整うまで保留されています。

FixedCapacityArraySmallArray

InlineArray は「固定サイズ・全要素が常に初期化済み」という型ですが、これと並ぶ形で次の 2 つの型が将来候補として挙げられています。

  • FixedCapacityArray<Capacity, Element>: 固定容量のままインライン確保しつつ、append / remove で要素数が動く配列。先頭から一定範囲だけが初期化済みという状態を取る。
  • SmallArray<Capacity, Element>: 要素数が Capacity 以下のうちはインライン、超えたらヒープ確保のバッキングに切り替わる配列。
public enum SmallArray<let Capacity: Int, Element: ~Copyable>: ~Copyable {
  case small(FixedCapacityArray<Capacity, Element>)
  case large(HypoArray<Element>)
}

FixedCapacityArray を実現するには「どこまでが初期化済みか」をコピー・ムーブ・破棄の各処理に伝える言語サポートが必要で、現時点ではそうした基盤が揃っていないため、いずれも検討段階に留まっています。

[T; N] のようなシンタックスシュガー

InlineArrayArrayDictionary と並ぶ基本型になることが見込まれており、[N x T] / [T; N](Rust 風)/ T[N](C 風)といったシンタックスシュガーの導入が候補として挙げられています。具体的な綴りは本 Proposal のスコープ外で、別途議論されます。

C 配列のインポート

char[16] のような C の固定長配列は、現状ではタプル((CChar, CChar, ...))としてインポートされ、文字列化や反復に withUnsafePointer などの unsafe API を要するなど扱いづらい状況にあります。これを InlineArray としてインポートできるようにする改善が検討されていますが、既存コードへの影響が大きく、二重インポートや段階的な移行などの方針が定まらないため、本 Proposal では扱われません。