authorgravatar for andrew.kraevskii@gmail.comAndrew Kraevskiii <andrew.kraevskii@gmail.com> 2026-03-12 20:50:27+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-12 20:50:27+01:00
log47597a6d7cb7bbea313c15ba4c9f5dcecb972e64
tree95678afa1f15e027bfc8a581d6923bf4465039af
parentd5bfa657c48e9d023bb789fbf8dacbcbd42f528d

std.ArrayList: add toOwnedSliceAssert, shrinkToLen. (#30769)

Adds a set of new APIs to ArrayList with goal of making it easier to avoid toOwnedSlice foot gun: ```zig fn wrongUsageOfCurrentApi(gpa: std.mem.Allocator) ![2][]const u8 { var a: std.ArrayList(u8) = .empty; defer a.deinit(gpa); var b: std.ArrayList(u8) = .empty; defer b.deinit(gpa); // ... return .{ try a.toOwnedSlice(gpa), try b.toOwnedSlice(gpa), // oom here causes leak :( }; } fn correctUsageOfCurrentApi(gpa: std.mem.Allocator) ![2][]const u8 { var a: std.ArrayList(u8) = .empty; defer a.deinit(gpa); var b: std.ArrayList(u8) = .empty; defer b.deinit(gpa); // ... const a_slice = try a.toOwnedSlice(gpa); errdefer gpa.free(a_slice); const b_slice = try b.toOwnedSlice(gpa); // may be omited but good if error appear bellow later in development. errdefer gpa.free(b_slice); return .{ a_slice, b_slice, }; } fn proposedApi(gpa: std.mem.Allocator) ![2][]const u8 { var a: std.ArrayList(u8) = .empty; defer a.deinit(gpa); var b: std.ArrayList(u8) = .empty; defer b.deinit(gpa); // ... try a.shrinkToLen(gpa); try b.shrinkToLen(gpa); return .{ a.toOwnedSliceAssert(), b.toOwnedSliceAssert(), }; } ``` Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30769 Reviewed-by: Andrew Kelley <andrew@ziglang.org> Co-authored-by: andrew.kraevskiii <andrew.kraevskii@gmail.com> Co-committed-by: andrew.kraevskiii <andrew.kraevskii@gmail.com>

1 files changed, 124 insertions(+), 7 deletions(-)

lib/std/array_list.zig+124-7
...@@ -674,6 +674,26 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {...@@ -674,6 +674,26 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
674 return result[0 .. result.len - 1 :sentinel];674 return result[0 .. result.len - 1 :sentinel];
675 }675 }
676676
677 /// The caller owns the returned memory. Empties this ArrayList.
678 /// Its capacity is cleared, making deinit() safe but unnecessary to call.
679 ///
680 /// Asserts what the capacity is equal to the length.
681 pub fn toOwnedSliceAssert(self: *Self) Slice {
682 assert(self.items.len == self.capacity);
683 const items = self.items;
684 self.* = .empty;
685 return items;
686 }
687
688 /// The caller owns the returned memory. ArrayList becomes empty.
689 /// Asserts what the capacity is equal to the length + 1.
690 pub fn toOwnedSliceSentinelAssert(self: *Self, comptime sentinel: T) SentinelSlice(sentinel) {
691 std.debug.assert(self.items.len + 1 == self.capacity);
692 self.appendAssumeCapacity(sentinel);
693 const result = self.toOwnedSliceAssert();
694 return result[0 .. result.len - 1 :sentinel];
695 }
696
677 /// Creates a copy of this ArrayList.697 /// Creates a copy of this ArrayList.
678 pub fn clone(self: Self, gpa: Allocator) Allocator.Error!Self {698 pub fn clone(self: Self, gpa: Allocator) Allocator.Error!Self {
679 var cloned = try Self.initCapacity(gpa, self.capacity);699 var cloned = try Self.initCapacity(gpa, self.capacity);
...@@ -1106,6 +1126,20 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {...@@ -1106,6 +1126,20 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
1106 /// May invalidate element pointers.1126 /// May invalidate element pointers.
1107 /// Asserts that the new length is less than or equal to the previous length.1127 /// Asserts that the new length is less than or equal to the previous length.
1108 pub fn shrinkAndFree(self: *Self, gpa: Allocator, new_len: usize) void {1128 pub fn shrinkAndFree(self: *Self, gpa: Allocator, new_len: usize) void {
1129 self.shrinkAndFreePrecise(gpa, new_len) catch |e| switch (e) {
1130 error.OutOfMemory => {
1131 // No problem, capacity is still correct then.
1132 self.items.len = new_len;
1133 return;
1134 },
1135 };
1136 }
1137
1138 /// Reduce allocated capacity to `new_len`.
1139 /// May invalidate element pointers.
1140 /// Asserts that the new length is less than or equal to the previous length.
1141 /// If succeds capacity is guaranteed to be equal to the length.
1142 pub fn shrinkAndFreePrecise(self: *Self, gpa: Allocator, new_len: usize) Allocator.Error!void {
1109 assert(new_len <= self.items.len);1143 assert(new_len <= self.items.len);
11101144
1111 if (@sizeOf(T) == 0) {1145 if (@sizeOf(T) == 0) {
...@@ -1120,13 +1154,7 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {...@@ -1120,13 +1154,7 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
1120 return;1154 return;
1121 }1155 }
11221156
1123 const new_memory = gpa.alignedAlloc(T, alignment, new_len) catch |e| switch (e) {1157 const new_memory = try gpa.alignedAlloc(T, alignment, new_len);
1124 error.OutOfMemory => {
1125 // No problem, capacity is still correct then.
1126 self.items.len = new_len;
1127 return;
1128 },
1129 };
11301158
1131 @memcpy(new_memory, self.items[0..new_len]);1159 @memcpy(new_memory, self.items[0..new_len]);
1132 gpa.free(old_memory);1160 gpa.free(old_memory);
...@@ -1134,6 +1162,32 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {...@@ -1134,6 +1162,32 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
1134 self.capacity = new_memory.len;1162 self.capacity = new_memory.len;
1135 }1163 }
11361164
1165 /// Shrinks capacity to match length.
1166 /// May invalidate element pointers.
1167 /// If succeds it is safe to call toOwnedSliceAssert().
1168 pub fn shrinkToLen(self: *Self, gpa: Allocator) Allocator.Error!void {
1169 try self.shrinkAndFreePrecise(gpa, self.items.len);
1170 }
1171
1172 /// Shrinks or expands capacity to match length + 1.
1173 /// May invalidate element pointers.
1174 /// If succeds it is safe to call toOwnedSliceSentinelAssert().
1175 pub fn shrinkToLenSentinel(self: *Self, gpa: Allocator) Allocator.Error!void {
1176 std.debug.assert(self.items.len <= self.capacity);
1177 const required_len = self.items.len + 1;
1178 switch (std.math.order(required_len, self.capacity)) {
1179 .eq => return,
1180 .gt => {
1181 try self.ensureTotalCapacityPrecise(gpa, required_len);
1182 },
1183 .lt => {
1184 self.items.len += 1;
1185 defer self.items.len -= 1;
1186 try self.shrinkToLen(gpa);
1187 },
1188 }
1189 }
1190
1137 /// Reduce length to `new_len`.1191 /// Reduce length to `new_len`.
1138 /// Invalidates pointers to elements `items[new_len..]`.1192 /// Invalidates pointers to elements `items[new_len..]`.
1139 /// Keeps capacity the same.1193 /// Keeps capacity the same.
...@@ -2089,6 +2143,30 @@ test "shrinkAndFree with a copy" {...@@ -2089,6 +2143,30 @@ test "shrinkAndFree with a copy" {
2089 try testing.expect(mem.eql(i32, list.items, &.{ 3, 3, 3, 3 }));2143 try testing.expect(mem.eql(i32, list.items, &.{ 3, 3, 3, 3 }));
2090}2144}
20912145
2146test "shrinkAndFreePrecise without resize succeeds" {
2147 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0 });
2148 const a = failing_allocator.allocator();
2149
2150 var list: Aligned(i32, null) = .empty;
2151 defer list.deinit(a);
2152
2153 try list.appendNTimes(a, 3, 16);
2154 try list.shrinkAndFreePrecise(a, 4);
2155 try testing.expectEqualSlices(i32, &.{ 3, 3, 3, 3 }, list.items);
2156 try testing.expectEqual(list.items.len, list.capacity);
2157}
2158
2159test "shrinkAndFreePrecise without resize and no copy failes" {
2160 var failing_allocator = testing.FailingAllocator.init(testing.allocator, .{ .resize_fail_index = 0, .fail_index = 1 });
2161 const a = failing_allocator.allocator();
2162
2163 var list: Aligned(i32, null) = .empty;
2164 defer list.deinit(a);
2165
2166 try list.appendNTimes(a, 3, 16);
2167 try std.testing.expectError(error.OutOfMemory, list.shrinkAndFreePrecise(a, 4));
2168}
2169
2092test "addManyAsArray" {2170test "addManyAsArray" {
2093 const a = std.testing.allocator;2171 const a = std.testing.allocator;
2094 {2172 {
...@@ -2219,6 +2297,45 @@ test "toOwnedSliceSentinel" {...@@ -2219,6 +2297,45 @@ test "toOwnedSliceSentinel" {
2219 }2297 }
2220}2298}
22212299
2300test "toOwnedSliceAssert" {
2301 var failing_allocator: testing.FailingAllocator = .init(testing.allocator, .{
2302 .fail_index = 2,
2303 });
2304 const a = failing_allocator.allocator();
2305
2306 var list: Aligned(u8, null) = try .initCapacity(a, 6); // first alloc
2307 list.appendSliceAssumeCapacity(&.{ 1, 2, 3 });
2308
2309 try list.shrinkToLen(a); // first resize
2310 try std.testing.expectEqual(list.items.len, list.capacity);
2311 try list.shrinkToLen(a); // no alloc or resize
2312
2313 const slice = list.toOwnedSliceAssert();
2314 defer a.free(slice);
2315
2316 try std.testing.expectEqual(Aligned(u8, null).empty, list);
2317 try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, slice);
2318}
2319
2320test "toOwnedSliceSentinelAssert" {
2321 const a = testing.allocator;
2322
2323 var list: Aligned(u8, null) = try .initCapacity(a, 6);
2324 list.appendSliceAssumeCapacity(&.{ 1, 2, 3 });
2325
2326 // shrinkToLenSentinel shrinks array
2327 try list.shrinkToLenSentinel(a);
2328
2329 // shrinkToLenSentinel expands array
2330 try list.shrinkToLen(a);
2331 try list.shrinkToLenSentinel(a);
2332
2333 const slice = list.toOwnedSliceSentinelAssert(10);
2334 defer a.free(slice);
2335
2336 try std.testing.expectEqualSentinel(u8, 10, &.{ 1, 2, 3 }, slice);
2337}
2338
2222test "accepts unaligned slices" {2339test "accepts unaligned slices" {
2223 const a = testing.allocator;2340 const a = testing.allocator;
2224 {2341 {