Zig 標準ライブラリで用意されているコレクション型をざっくり押さえる
Zig の標準ライブラリではコレクション型が提供されていますが、ドキュメント整備が途上ということもあり、どのようなコレクションが提供されているのかを把握するのが難しいと感じています。
提供されているコレクションを簡単にまとめ、適切な場面で適切なコレクション型を利用することができるようになることを狙いとした記事です。
なお、対象のZigバージョンは 0.10.0-dev.3513+e218b7ea0 です。
また、掲載しているサンプルコードは以下のgistにまとめてあります。
ArrayList
全要素がメモリ上で連続した領域に配置され、実行時に動的に長さを変えることができるコレクションです。
C++のstd::vector
, Rustのstd::vec::Vec
に相当します。
添字によるランダムアクセス、末尾への要素追加、順方向・逆方向のイテレーションをしたいユースケースで役立ちます。
サンプルコード
test "ArrayList" {
var list = ArrayList(usize).init(testing.allocator);
defer list.deinit();
{
var i: usize = 0;
while (i < 10) : (i += 1) {
try list.append(i);
}
}
for (list.items) |v, i| {
try testing.expectEqual(v, i);
}
try testing.expectEqual(@as(usize, 9), list.popOrNull().?);
try testing.expectEqual(@as(usize, 9), list.items.len);
try testing.expectEqual(@as(usize, 4), list.items[4]);
}
MultiArrayList
構造体を要素とする ArrayList を作りたいときは MultiArrayList
の利用を検討してみてください。以下のような構造体 S
があったときに
const S = struct {
a: u32,
b: []const u8,
};
以下のように S
をまとめて ArrayList に格納するというやり方があります。
S をまるごとメモリの連続領域に格納
+-----------------------
| S_1 | S_2 | S_3 | ...
+-----------------------
これは ArrayList(S)
に相当します。
一方、以下のように、S
の各フィールドを別々のリストで管理するやり方があります。こちらが MultiArrayList(S)
に相当します。
フィールド a をメモリの連続領域に格納
+-----------------------
| a_1 | a_2 | a_3 | ...
+-----------------------
フィールド b をメモリの別の連続領域に格納
+-----------------------
| b_1 | b_2 | b_3 | ...
+-----------------------
このようにフィールドごとに別々のリストを使って値を管理することで、メモリの節約[1]やキャッシュ利用の効率化[2]を図るのが MultiArrayList
です。
サンプルコード
test "MultiArrayList" {
const allocator = testing.allocator;
const Foo = struct {
field_one: u32,
field_two: []const u8,
};
var list = MultiArrayList(Foo){};
defer list.deinit(allocator);
try testing.expectEqual(@as(usize, 0), list.items(.field_one).len);
try list.append(allocator, .{
.field_one = 1,
.field_two = "foo",
});
try list.append(allocator, .{
.field_one = 2,
.field_two = "bar",
});
try testing.expectEqualSlices(u32, list.items(.field_one), &[_]u32{ 1, 2 });
try testing.expectEqual(@as(usize, 2), list.items(.field_two).len);
try testing.expectEqualStrings("foo", list.items(.field_two)[0]);
try testing.expectEqualStrings("bar", list.items(.field_two)[1]);
}
SegmentedList
ArrayList
と同様に、高速なランダムアクセスと末尾への追加・削除を行うことができるデータ構造です。
違いは、ArrayList
は確保したメモリ領域が足りなくなった場合は、別の大きな領域を確保して、すべての要素をそこにコピーするのに対して、SegmentedList
では、追加領域を確保したあとも、それまで格納されていたデータは元の位置に残るというところにあります。
つまり、ArrayList
はすべての要素が順番にメモリ上に並んでいるのが保証されていますが、SegmentedList
ではある程度の大きさの要素の「固まり」ごとに、別のメモリ領域に配置されることになります。
SegmentedList
のメリットは、要素を指すポインタが生存する期間が、SegmentedList
自体のライフタイムと一致することです。さきほど書いたように、ArrayList
では、確保したメモリ領域が足りなくなったときに他の領域へのコピーが行われるため、元の領域を指すポインタはこの時点で不正となります。一方、SegmentedList
では、メモリ領域が足りなくなった場合にも元の要素のコピーは行われないので、ポインタが不正になることはありません。
サンプルコード
test "SegmentedList" {
const L = SegmentedList(u32, 2);
var list = L{};
defer list.deinit(testing.allocator);
try list.append(testing.allocator, 1);
try list.append(testing.allocator, 2);
try list.append(testing.allocator, 3);
try testing.expectEqual(@as(usize, 3), list.count());
{
var it = list.iterator(0);
var s: u32 = 0;
while (it.next()) |item| {
s += item.*;
}
try testing.expectEqual(@as(u32, 6), s);
}
{
var it = list.constIterator(0);
var s: u32 = 0;
while (it.next()) |item| {
s += item.*;
}
try testing.expectEqual(@as(u32, 6), s);
}
}
SinglyLinkedList
片方向連結リストです。C++の std::forward_list
に相当します。
要素のメモリ、ライフタイムの管理は呼び出し側の責任です。
サンプルコード
test "SinglyLinkedList" {
const L = SinglyLinkedList(u32);
var list = L{};
try testing.expectEqual(@as(usize, 0), list.len());
var one = L.Node{ .data = 1 };
var two = L.Node{ .data = 2 };
list.prepend(&two);
list.prepend(&one);
{
var it = list.first;
var val: u32 = 1;
while (it) |node| : (it = node.next) {
try testing.expectEqual(val, node.data);
val += 1;
}
}
try testing.expectEqual(@as(usize, 2), list.len());
try testing.expectEqual(@as(u32, 1), list.first.?.data);
try testing.expectEqual(@as(u32, 1), list.popFirst().?.data);
}
TailQueue
双方向連結リストです。C++の std::list
、Rust の std::collections::LinkedList
に相当します。
要素のメモリ、ライフタイムの管理は呼び出し側の責任です。
サンプルコード
test "TailQueue" {
const L = TailQueue(u32);
var list = L{};
try testing.expectEqual(@as(usize, 0), list.len);
var one = L.Node{ .data = 1 };
var two = L.Node{ .data = 2 };
var three = L.Node{ .data = 3 };
list.append(&two);
list.append(&three);
list.prepend(&one);
try testing.expectEqual(@as(usize, 3), list.len);
// 順方向イテレート
{
var it = list.first;
var val: u32 = 1;
while (it) |node| : (it = node.next) {
try testing.expectEqual(val, node.data);
val += 1;
}
}
// 逆方向イテレート
{
var it = list.last;
var val: u32 = 3;
while (it) |node| : (it = node.prev) {
try testing.expectEqual(val, node.data);
val -= 1;
}
}
}
HashMap
いわゆる連想配列です。C++のstd::unordered_map
、 Rust のstd::collections::HashMap
に相当します。
基本的には AutoHashMap
という Auto という prefix がついたものを使うことになると思います。こちらはハッシュ関数やキーの同値判定を行う関数をよしなに生成してくれます。
逆に、ハッシュ関数やキーの同値判定をカスタマイズしたい場合は、HashMap
を使います。
サンプルコード
test "AutoHashMap" {
var map = AutoHashMap(u32, u8).init(testing.allocator);
defer map.deinit();
try map.put(0, 'a');
try map.put(1, 'b');
try testing.expectEqual(@as(u8, 'a'), map.get(0).?);
try testing.expectEqual(@as(u8, 'b'), map.get(1).?);
try testing.expect(map.get(2) == null);
const prev = try map.fetchPut(0, 'x');
try testing.expectEqual(@as(u32, 0), prev.?.key);
try testing.expectEqual(@as(u8, 'a'), prev.?.value);
}
ArrayHashMap
挿入順が保持される HashMap です。挿入順を保持したままイテレーションしたい場合や、イテレーション時のパフォーマンスを重視する場合は HashMap
よりも ArrayHashMap
を利用するのが良いようです。
HashMap と同様に、AutoArrayHashMap
も用意されていて、基本的なユースケースではこちらで事足りるかと思います。
サンプルコード
test "AutoArrayHashMap" {
var map = AutoArrayHashMap(u32, u8).init(testing.allocator);
defer map.deinit();
try map.put(0, 'a');
try map.put(1, 'b');
var it = map.iterator();
try testing.expectEqual(@as(u8, 'a'), it.next().?.value_ptr.*);
try testing.expectEqual(@as(u8, 'b'), it.next().?.value_ptr.*);
try testing.expect(it.next() == null);
}
StringHashMap
, StringArrayHashMap
キーが文字列 ([]const u8
) の場合はこれらを利用します。なお、キーとなる文字列のメモリを管理するのは呼び出し側の責任です。管理するデータのライフタイムをデータ構造と同一にしたい場合は、次に紹介する BufMap
の利用を検討してください。
使い方は HashMap
, ArrayHashMap
と同じなので割愛します。
BufMap
, BufSet
BufMap
は、キー、バリューがともに []const u8
であるような HashMap です。
ただし、キー・バリューを追加するときに、文字列をコピーしてそのライフタイムを BufMap
内で管理するようにする、という違いがあります[3]。
例えば、BufMap
を使っている場合、値の上書きが発生する際、上書きされる側のメモリ領域を自動で適切に解放してくれます。また、deinit()
メソッドによって、格納されているすべてのキー、バリューのメモリ領域が解放されます。
BufSet
はキーが []const u8
、バリューが void
であるような HashMap で、文字列の集合を表現したいときに利用できます。
メモリ管理については BufMap
と同様です。
サンプルコード
test "BufMap" {
var bufmap = BufMap.init(testing.allocator);
defer bufmap.deinit();
try bufmap.put("x", "1");
try testing.expect(mem.eql(u8, bufmap.get("x").?, "1"));
try testing.expect(1 == bufmap.count());
try bufmap.put("x", "2");
try testing.expect(mem.eql(u8, bufmap.get("x").?, "2"));
try testing.expect(1 == bufmap.count());
bufmap.remove("x");
try testing.expect(0 == bufmap.count());
}
test "BufSet" {
var bufset = BufSet.init(testing.allocator);
defer bufset.deinit();
try bufset.insert("x");
try testing.expect(bufset.count() == 1);
try testing.expect(bufset.contains("x"));
bufset.remove("x");
try testing.expect(bufset.count() == 0);
}
ComptimeStringMap
キーが []const u8
で、コンパイル時にすべての要素が確定するような Map が欲しい場合、ComptimeStringMap
の出番です。
コンパイル時に前計算を行うことで、実行時のlookup処理の最適化を行います。具体的には、lookup時に、キーとなる文字列の長さが一致する部分のみを探索するようになります。例えば、map.get("foo")
というようにしたら、キーの長さが3のもののみを探索します。
サンプルコード
test "ComptimeStringMap" {
const KV = struct {
@"0": []const u8,
@"1": u32,
};
const map = ComptimeStringMap(u32, [_]KV{
.{ .@"0" = "foo", .@"1" = 42 },
.{ .@"0" = "barbaz", .@"1" = 99 },
});
try testing.expectEqual(@as(u32, 42), map.get("foo").?);
try testing.expectEqual(@as(u32, 99), map.get("barbaz").?);
try testing.expect(!map.has("hello"));
try testing.expect(map.get("hello") == null);
}
BoundedArray
正確なサイズは実行時になるまで不明だが、最大サイズはコンパイル時に分かっている、というような配列が欲しいときに便利なデータ構造です。
コンパイル時に最大サイズ分のメモリ領域を確保するため、アロケータが不要です。
アイデアとしてはRust の smallvec
に近いです。ただし smallvec
は、事前に確保した配列サイズを超えるとヒープ領域に移動してくれますが、BoundedArray
は、指定した最大サイズを超える要素数を指定すると、Overflow
エラーになります。
サンプルコード
test "BoundedArray" {
const BoundedArrayMax4 = BoundedArray(u8, 4);
try testing.expectError(error.Overflow, BoundedArrayMax4.init(8));
var a = try BoundedArrayMax4.init(2);
try testing.expectEqual(a.capacity(), 4);
try testing.expectEqual(a.len, 2);
try testing.expectEqual(a.slice().len, 2);
try testing.expectEqual(a.constSlice().len, 2);
try a.resize(4);
try testing.expectEqual(a.len, 4);
a.set(0, 42);
try testing.expectEqual(a.get(0), 42);
}
StaticBitSet
, DynamicBitSet
ビット集合を表すデータ構造です。C++の std::bitset
に相当します。
コンパイル時にビットサイズが決定する場合は StaticBitSet
を、実行時に決定する場合は DynamicBitSet
を利用します。
ビットサイズによって最適なデータ構造が選択されます。ビットサイズが小さい場合は整数を利用し、大きい場合は配列が使われます。
サンプルコード
test "StaticBitSet" {
var bitset = StaticBitSet(4).initEmpty();
try testing.expectEqual(@as(usize, 0), bitset.count());
bitset.setValue(1, true);
try testing.expectEqual(@as(usize, 1), bitset.count());
try testing.expect(!bitset.isSet(0));
try testing.expect(bitset.isSet(1));
bitset.setRangeValue(.{ .start = 2, .end = 4 }, true);
try testing.expectEqual(@as(usize, 3), bitset.count());
}
test "DynamicBitSet" {
const size = @intCast(usize, time.timestamp()) % 60;
var bitset = try DynamicBitSet.initEmpty(std.testing.allocator, size);
defer bitset.deinit();
try testing.expectEqual(@as(usize, 0), bitset.count());
bitset.toggleAll();
try testing.expectEqual(size, bitset.count());
}
EnumArray
, EnumMap
, EnumSet
Enum をキー(インデックス)とする Array, Map, Set が欲しい場合に便利なデータ構造です。
サンプルコード
test "EnumArray" {
const A = EnumArray(enum {
foo,
bar,
}, u32);
try testing.expectEqual(@as(usize, 2), A.len);
var a = A.initFill(42);
try testing.expectEqual(@as(u32, 42), a.get(.foo));
try testing.expectEqual(@as(u32, 42), a.get(.bar));
}
test "EnumMap" {
const A = EnumMap(enum {
foo,
bar,
}, u32);
try testing.expectEqual(@as(usize, 2), A.len);
var a = A{};
try testing.expectEqual(@as(usize, 0), a.count());
a.put(.foo, 42);
try testing.expectEqual(@as(usize, 1), a.count());
try testing.expect(a.contains(.foo));
try testing.expect(!a.contains(.bar));
}
test "EnumSet" {
const A = EnumSet(enum {
foo,
bar,
});
try testing.expectEqual(@as(usize, 2), A.len);
var a = A{};
try testing.expectEqual(@as(usize, 0), a.count());
a.insert(.foo);
try testing.expectEqual(@as(usize, 1), a.count());
try testing.expect(a.contains(.foo));
try testing.expect(!a.contains(.bar));
a.remove(.foo);
try testing.expectEqual(@as(usize, 0), a.count());
}
PriorityQueue
, PriorityDequeue
優先度付きキューです。C++のstd::priority_queue
や Rust の std::collections::BinaryHeap
に相当します。
最大値、もしくは最小値のみを高速に取り出したい場合は PriorityQueue
を使い、最大値と最小値の両方を高速に取り出したい場合は PriorityDequeue
を利用します。
サンプルコード
test "PriorityQueue" {
{
const MinHeap = PriorityQueue(u32, void, struct {
fn lessThan(context: void, a: u32, b: u32) math.Order {
_ = context;
return math.order(a, b);
}
}.lessThan);
var queue = MinHeap.init(testing.allocator, {});
defer queue.deinit();
try queue.add(12);
try queue.add(7);
try queue.add(23);
try testing.expectEqual(@as(usize, 3), queue.len);
try testing.expectEqual(@as(u32, 7), queue.remove());
try testing.expectEqual(@as(u32, 12), queue.remove());
try testing.expectEqual(@as(u32, 23), queue.remove());
}
{
const MaxHeap = PriorityQueue(u32, void, struct {
fn greaterThan(context: void, a: u32, b: u32) math.Order {
_ = context;
return math.order(a, b).invert();
}
}.greaterThan);
var queue = MaxHeap.init(testing.allocator, {});
defer queue.deinit();
try queue.add(12);
try queue.add(7);
try queue.add(23);
try testing.expectEqual(@as(usize, 3), queue.len);
try testing.expectEqual(@as(u32, 23), queue.remove());
try testing.expectEqual(@as(u32, 12), queue.remove());
try testing.expectEqual(@as(u32, 7), queue.remove());
}
}
test "PriorityDequeue" {
const PQ = PriorityDequeue(u32, void, struct {
fn lessThan(context: void, a: u32, b: u32) math.Order {
_ = context;
return math.order(a, b);
}
}.lessThan);
var queue = PQ.init(testing.allocator, {});
defer queue.deinit();
try queue.add(12);
try queue.add(7);
try queue.add(23);
try testing.expectEqual(@as(usize, 3), queue.len);
try testing.expectEqual(@as(u32, 7), queue.removeMin());
try testing.expectEqual(@as(u32, 23), queue.removeMax());
try testing.expectEqual(@as(u32, 12), queue.removeMin());
}
Treap
平衡二分探索木です。Treap - Wikipedia
赤黒木などの他の平衡二分探索木ではなく Treap
が採用されているのは、実装のシンプルさというのが最大の理由とのことです。
サンプルコード
test "Treap" {
const MyTreap = Treap(u32, math.order);
const Node = MyTreap.Node;
var treap = MyTreap{};
var nodes: [10]Node = undefined;
var i: u32 = 0;
while (i < 10) : (i += 1) {
var entry = treap.getEntryFor(i);
try testing.expectEqual(i, entry.key);
try testing.expect(entry.node == null);
entry.set(&nodes[i]);
}
try testing.expectEqual(@as(u32, 9), treap.getMax().?.key);
try testing.expectEqual(@as(u32, 0), treap.getMin().?.key);
}
おまけ
C++ の std::deque
、Rustの std::collections::VecDeque
に相当するデータ構造は提供されていないということに気づき、ざっくりと自作してみました。
ArrayList
と同じくランダムアクセスと末尾への追加・削除が高速に行えるのに加えて、先頭への追加・削除も効率的に行うことができるデータ構造です。
Discussion