dispenso 1.6.2
A library for task parallelism
Loading...
Searching...
No Matches
small_vector.h
Go to the documentation of this file.
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
22#pragma once
23
24#include <cassert>
25#include <cstddef>
26#include <functional>
27#include <initializer_list>
28#include <new>
29#include <utility>
30
31#include <dispenso/platform.h>
32
33namespace dispenso {
34
50template <typename T, size_t N = 4>
52 static_assert(N > 0, "SmallVector requires at least 1 inline element. Use std::vector for N=0.");
53 static_assert(N < 65536, "SmallVector inline capacity is too large. Use std::vector instead.");
54
55 public:
56 using value_type = T;
57 using size_type = size_t;
58 using difference_type = std::ptrdiff_t;
59 using reference = T&;
60 using const_reference = const T&;
61 using pointer = T*;
62 using const_pointer = const T*;
63 using iterator = T*;
64 using const_iterator = const T*;
65
69 SmallVector() noexcept : size_(0) {}
70
76 explicit SmallVector(size_type count) : size_(0) {
77 constructOrRelease([&]() { resize(count); });
78 }
79
86 SmallVector(size_type count, const T& value) : size_(0) {
87 constructOrRelease([&]() { resize(count, value); });
88 }
89
95 SmallVector(std::initializer_list<T> init) : size_(0) {
96 constructOrRelease([&]() {
97 ensureCapacity(init.size());
98 for (const auto& v : init) {
99 emplace_back(v);
100 }
101 });
102 }
103
105 SmallVector(const SmallVector& other) : size_(0) {
106 constructOrRelease([&]() {
107 ensureCapacity(other.rawSize());
108 for (const auto& v : other) {
109 emplace_back(v);
110 }
111 });
112 }
113
115 SmallVector(SmallVector&& other) noexcept : size_(0) {
116 if (other.isInline()) {
117 // Inline means the heap bit is clear, so size_ is already the count, and
118 // hoisting it keeps the stores below from forcing a reload each pass.
119 const size_type n = other.size_;
120 for (size_type i = 0; i < n; ++i) {
121 new (inlineData() + i) T(std::move(other.inlineData()[i]));
122 other.inlineData()[i].~T();
123 }
124 } else {
125 storage_.heap_.ptr = other.storage_.heap_.ptr;
126 storage_.heap_.capacity = other.storage_.heap_.capacity;
127 }
128 // Count and heap bit both transfer verbatim.
129 size_ = other.size_;
130 other.size_ = 0;
131 }
132
133 ~SmallVector() {
134 destroyAll();
135 }
136
138 SmallVector& operator=(const SmallVector& other) {
139 if (this != &other) {
140 destroyAll();
141 size_ = 0;
142 ensureCapacity(other.rawSize());
143 for (const auto& v : other) {
144 emplace_back(v);
145 }
146 }
147 return *this;
148 }
149
151 SmallVector& operator=(SmallVector&& other) noexcept {
152 if (this != &other) {
153 destroyAll();
154
155 if (other.isInline()) {
156 // Inline means the heap bit is clear, so size_ is already the count,
157 // and hoisting it keeps the stores below from forcing a reload each
158 // pass.
159 const size_type n = other.size_;
160 for (size_type i = 0; i < n; ++i) {
161 new (inlineData() + i) T(std::move(other.inlineData()[i]));
162 other.inlineData()[i].~T();
163 }
164 } else {
165 storage_.heap_.ptr = other.storage_.heap_.ptr;
166 storage_.heap_.capacity = other.storage_.heap_.capacity;
167 }
168 // Count and heap bit both transfer verbatim. Nothing between destroyAll()
169 // and here reads size_: the inline branch goes through inlineData()
170 // rather than data(), and the heap branch overwrites the freed pointer
171 // before anything can observe it.
172 size_ = other.size_;
173 other.size_ = 0;
174 }
175 return *this;
176 }
177
178 // --- Element Access ---
179
181 reference operator[](size_type pos) {
182 return data()[pos];
183 }
185 const_reference operator[](size_type pos) const {
186 return data()[pos];
187 }
189 reference front() {
190 return data()[0];
191 }
193 const_reference front() const {
194 return data()[0];
195 }
197 reference back() {
198 return data()[rawSize() - 1];
199 }
201 const_reference back() const {
202 return data()[rawSize() - 1];
203 }
204
206 pointer data() noexcept {
207 return isInline() ? inlineData() : storage_.heap_.ptr;
208 }
210 const_pointer data() const noexcept {
211 return isInline() ? inlineData() : storage_.heap_.ptr;
212 }
213
214 // --- Iterators ---
215
217 iterator begin() noexcept {
218 return data();
219 }
221 const_iterator begin() const noexcept {
222 return data();
223 }
225 const_iterator cbegin() const noexcept {
226 return data();
227 }
229 iterator end() noexcept {
230 return data() + rawSize();
231 }
233 const_iterator end() const noexcept {
234 return data() + rawSize();
235 }
237 const_iterator cend() const noexcept {
238 return data() + rawSize();
239 }
240
241 // --- Capacity ---
242
244 bool empty() const noexcept {
245 return rawSize() == 0;
246 }
248 size_type size() const noexcept {
249 return rawSize();
250 }
251
256 size_type capacity() const noexcept {
257 return isInline() ? N : storage_.heap_.capacity;
258 }
259
265 void reserve(size_type newCap) {
266 ensureCapacity(newCap);
267 }
268
269 // --- Modifiers ---
270
275 void clear() noexcept {
276 destroyAll();
277 size_ = 0;
278 }
279
281 void push_back(const T& value) {
282 emplace_back(value);
283 }
285 void push_back(T&& value) {
286 emplace_back(std::move(value));
287 }
288
295 template <typename... Args>
296 reference emplace_back(Args&&... args) {
297 T* ptr;
298 if (isInline()) {
299 size_type sz = rawSize();
300 if (sz < N) {
301 ptr = inlineData();
302 } else {
303 return growAndEmplaceBack(N * 2, std::forward<Args>(args)...);
304 }
305 } else {
306 size_type sz = rawSize();
307 if (sz == storage_.heap_.capacity) {
308 return growAndEmplaceBack(storage_.heap_.capacity * 2, std::forward<Args>(args)...);
309 }
310 ptr = storage_.heap_.ptr;
311 }
312 size_type idx = rawSize();
313 new (ptr + idx) T(std::forward<Args>(args)...);
314 // Increment preserves heap bit naturally
315 ++size_;
316 assert(rawSize() > 0 && "Size overflow into heap bit");
317 return ptr[idx];
318 }
319
321 void pop_back() {
322 T* ptr = data();
323 size_type sz = rawSize();
324 ptr[sz - 1].~T();
325 // Decrement preserves heap bit naturally
326 --size_;
327 }
328
334 void resize(size_type count) {
335 size_type sz = rawSize();
336 if (count > sz) {
337 ensureCapacity(count);
338 constructRange(data(), sz, count, [](T* p) { new (p) T(); });
339 setSize(count);
340 } else if (count < sz) {
341 T* ptr = data();
342 for (size_type i = count; i < sz; ++i) {
343 ptr[i].~T();
344 }
345 setSize(count);
346 }
347 }
348
356 void resize(size_type count, const T& value) {
357 size_type sz = rawSize();
358 if (count > sz) {
359 if (count > capacity() && isElement(&value)) {
360 // Growing moves the elements and releases their storage, so value would dangle; fill
361 // from a copy instead (as std::vector supports v.resize(n, v[0])).
362 const T copy(value);
363 resize(count, copy);
364 return;
365 }
366 ensureCapacity(count);
367 constructRange(data(), sz, count, [&value](T* p) { new (p) T(value); });
368 setSize(count);
369 } else if (count < sz) {
370 T* ptr = data();
371 for (size_type i = count; i < sz; ++i) {
372 ptr[i].~T();
373 }
374 setSize(count);
375 }
376 }
377
386 iterator erase(const_iterator pos) {
387 T* ptr = data();
388 size_type sz = rawSize();
389 size_type index = pos - ptr;
390
391 for (size_type i = index; i + 1 < sz; ++i) {
392 ptr[i] = std::move(ptr[i + 1]);
393 }
394 ptr[sz - 1].~T();
395 --size_; // Preserves heap bit
396 return data() + index;
397 }
398
399 private:
400 // High bit of size_ tracks heap vs inline mode.
401 // Since kHeapBit >> N, comparisons like size_ <= N and size_ < N
402 // are naturally false when the heap bit is set, so isInline() and
403 // many internal checks work without masking.
404 static constexpr size_type kHeapBit = size_type(1) << (sizeof(size_type) * 8 - 1);
405 static constexpr size_type kSizeMask = ~kHeapBit;
406
407 bool isInline() const noexcept {
408 return (size_ & kHeapBit) == 0;
409 }
410
411 size_type rawSize() const noexcept {
412 return size_ & kSizeMask;
413 }
414
415 // Set the size portion, preserving the heap bit.
416 void setSize(size_type s) noexcept {
417 assert((s & kHeapBit) == 0 && "Size overflow into heap bit");
418 size_ = (size_ & kHeapBit) | s;
419 }
420
421 T* inlineData() noexcept {
422 return reinterpret_cast<T*>(&storage_.inline_);
423 }
424 const T* inlineData() const noexcept {
425 return reinterpret_cast<const T*>(&storage_.inline_);
426 }
427
428 // Whether p points at one of this vector's elements. std::less gives a total order even for
429 // pointers into unrelated objects.
430 bool isElement(const T* p) const noexcept {
431 const T* begin = data();
432 return !std::less<const T*>()(p, begin) && std::less<const T*>()(p, begin + rawSize());
433 }
434
435 // For constructors: run fill(), and if it throws, destroy whatever it built and free any heap
436 // storage before propagating, since the destructor does not run for a constructor that throws.
437 template <typename Fill>
438 void constructOrRelease(Fill fill) {
439#if defined(__cpp_exceptions)
440 try {
441 fill();
442 } catch (...) {
443 destroyAll();
444 throw;
445 }
446#else
447 fill();
448#endif // __cpp_exceptions
449 }
450
451 // Construct ptr[begin, end) by calling construct(ptr + i) for each slot. If one throws, the
452 // elements already built in this range are destroyed before the exception propagates.
453 template <typename Construct>
454 static void constructRange(T* ptr, size_type begin, size_type end, Construct construct) {
455 size_type i = begin;
456#if defined(__cpp_exceptions)
457 try {
458#endif // __cpp_exceptions
459 for (; i < end; ++i) {
460 construct(ptr + i);
461 }
462#if defined(__cpp_exceptions)
463 } catch (...) {
464 for (size_type j = begin; j < i; ++j) {
465 ptr[j].~T();
466 }
467 throw;
468 }
469#endif // __cpp_exceptions
470 }
471
472 void destroyAll() noexcept {
473 T* ptr = data();
474 size_type sz = rawSize();
475 for (size_type i = 0; i < sz; ++i) {
476 ptr[i].~T();
477 }
478 if (!isInline()) {
479 deallocate(storage_.heap_.ptr);
480 }
481 }
482
483 // Heap storage for over-aligned T must honour alignof(T). An untyped ::operator new(size) call
484 // only guarantees the default new alignment, in every standard: C++17's over-aligned allocation
485 // applies to new-expressions, not to direct calls like these. Over-aligned T therefore goes
486 // through dispenso's aligned allocation.
487 // TODO: Once C++17 is the minimum, std::allocator<T> (which honours over-alignment since C++17)
488 // can replace both branches of allocate() and deallocate().
489 static constexpr bool kOverAligned = alignof(T) > alignof(std::max_align_t);
490
491 static T* allocate(size_type count) {
492 if (kOverAligned) {
493 return static_cast<T*>(detail::alignedMallocOrThrow(count * sizeof(T), alignof(T)));
494 }
495 return static_cast<T*>(::operator new(count * sizeof(T)));
496 }
497
498 static void deallocate(T* ptr) noexcept {
499 if (kOverAligned) {
500 detail::alignedFree(ptr);
501 return;
502 }
503 // GCC on 32-bit ARM reports this, called from destroyAll(), as freeing the
504 // inline buffer. It is a false positive: the heap bit lives inside size_
505 // and the buffer is a union member aliasing heap_.ptr, and across the
506 // inlined move paths GCC stops being able to prove the !isInline() guard
507 // excludes inline storage. The same code is clean under GCC on x86_64,
508 // x86-32, aarch64, riscv32 and riscv64, which is not what a genuine logic
509 // error would look like. Simplifying the move paths to a single size_
510 // assignment did not change its mind either.
511 DISPENSO_DISABLE_WARNING_PUSH
512 DISPENSO_DISABLE_WARNING_FREE_NONHEAP_OBJECT
513 ::operator delete(ptr);
514 DISPENSO_DISABLE_WARNING_POP
515 }
516
517 // Grow to heap storage with the specified capacity.
518 // Moves existing elements, frees old heap if applicable, sets heap bit. If transferring an
519 // element throws, the new storage is freed and the vector is left as it was.
520 void growToHeap(size_type newCap) {
521 T* newData = allocate(newCap);
522#if defined(__cpp_exceptions)
523 try {
524 transferTo(newData);
525 } catch (...) {
526 deallocate(newData);
527 throw;
528 }
529#else
530 transferTo(newData);
531#endif // __cpp_exceptions
532 adoptHeap(newData, newCap);
533 }
534
535 // Construct the existing elements in newData, moving them when T's move constructor cannot throw
536 // and copying them otherwise (std::move_if_noexcept, as std::vector does). If a construction
537 // throws, the elements constructed in newData so far are destroyed and the exception propagates
538 // with the existing elements intact (unless T is move-only with a throwing move constructor, in
539 // which case some of them may have been moved from). newData is left for the caller to free.
540 void transferTo(T* newData) {
541 T* oldData = data();
542 size_type sz = rawSize();
543 size_type i = 0;
544#if defined(__cpp_exceptions)
545 try {
546#endif // __cpp_exceptions
547 for (; i < sz; ++i) {
548 new (newData + i) T(std::move_if_noexcept(oldData[i]));
549 }
550#if defined(__cpp_exceptions)
551 } catch (...) {
552 for (size_type j = 0; j < i; ++j) {
553 newData[j].~T();
554 }
555 throw;
556 }
557#endif // __cpp_exceptions
558 }
559
560 // Commit after a successful transferTo(newData): destroy the old elements, free the old heap
561 // storage if any, and switch to heap mode using newData (capacity newCap).
562 void adoptHeap(T* newData, size_type newCap) noexcept {
563 T* oldData = data();
564 size_type sz = rawSize();
565
566 for (size_type i = 0; i < sz; ++i) {
567 oldData[i].~T();
568 }
569
570 if (!isInline()) {
571 deallocate(storage_.heap_.ptr);
572 }
573
574 storage_.heap_.ptr = newData;
575 storage_.heap_.capacity = newCap;
576 size_ = kHeapBit | sz;
577 }
578
579 // emplace_back() when full: grow to newCap and append. The new element is constructed in the new
580 // storage before the existing elements are moved out of the old storage, because args may refer
581 // to one of those elements (e.g. v.push_back(v[0])). If anything throws, the new storage is freed
582 // and the vector is left as it was.
583 template <typename... Args>
584 reference growAndEmplaceBack(size_type newCap, Args&&... args) {
585 T* newData = allocate(newCap);
586 size_type sz = rawSize();
587#if defined(__cpp_exceptions)
588 try {
589 new (newData + sz) T(std::forward<Args>(args)...);
590 } catch (...) {
591 deallocate(newData);
592 throw;
593 }
594 try {
595 transferTo(newData);
596 } catch (...) {
597 newData[sz].~T();
598 deallocate(newData);
599 throw;
600 }
601#else
602 new (newData + sz) T(std::forward<Args>(args)...);
603 transferTo(newData);
604#endif // __cpp_exceptions
605 adoptHeap(newData, newCap);
606 ++size_; // Preserves heap bit
607 assert(rawSize() > 0 && "Size overflow into heap bit");
608 return newData[sz];
609 }
610
611 void ensureCapacity(size_type newCap) {
612 if (newCap <= N && isInline()) {
613 return;
614 }
615 if (isInline()) {
616 growToHeap(newCap);
617 } else if (newCap > storage_.heap_.capacity) {
618 growToHeap(newCap);
619 }
620 }
621
622 size_type size_;
623
624 struct HeapStorage {
625 T* ptr;
626 size_type capacity;
627 };
628
629 union Storage {
630 alignas(T) unsigned char inline_[sizeof(T) * N];
631 HeapStorage heap_;
632 Storage() noexcept {}
633 ~Storage() {}
634 } storage_;
635};
636
637} // namespace dispenso
pointer data() noexcept
void reserve(size_type newCap)
SmallVector(const SmallVector &other)
void push_back(const T &value)
void resize(size_type count)
void push_back(T &&value)
const_iterator end() const noexcept
const_iterator cbegin() const noexcept
const_reference back() const
const_reference front() const
SmallVector(SmallVector &&other) noexcept
bool empty() const noexcept
SmallVector(std::initializer_list< T > init)
const_iterator begin() const noexcept
const_pointer data() const noexcept
size_type capacity() const noexcept
void resize(size_type count, const T &value)
reference emplace_back(Args &&... args)
const_iterator cend() const noexcept
iterator end() noexcept
SmallVector(size_type count, const T &value)
iterator erase(const_iterator pos)
size_type size() const noexcept
SmallVector(size_type count)
iterator begin() noexcept
void clear() noexcept