// zlib open source license // // Copyright (c) 2018 to 2025 David Forsgren Piuva // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. #ifndef DFPSR_COLLECTION_LIST #define DFPSR_COLLECTION_LIST #include "collections.h" #include "../base/Handle.h" #include "../base/DsrTraits.h" #include #include namespace dsr { // TODO: // * Allow storing names of lists in debug mode for better error messages, using the same setName method as used in Handle. // * Allow getting a SafePointer to all elements for faster loops without bound checks in release mode. // Because elements are returned by reference, the list can not know when an element is modified. // Therefore it must clone the entire content when assigned by value for consistent behavior during reallocation. // When cloning on assignment, const can be inherited from the outside for passing read only lists as const references. template class List { private: T *impl_elements = nullptr; intptr_t impl_length = 0; intptr_t impl_buffer_length = 0; // Makes sure that there is memory available for storing at least minimumAllocatedLength elements. void impl_reserve(intptr_t minimumAllocatedLength) { if (minimumAllocatedLength > this->impl_buffer_length) { // Create a new memory allocation. UnsafeAllocation newAllocation = (heap_allocate(minimumAllocatedLength * sizeof(T), true)); #ifdef SAFE_POINTER_CHECKS heap_setAllocationName(newAllocation.data, "List allocation"); #endif T *newElements = (T*)(newAllocation.data); heap_increaseUseCount(newAllocation.header); // Use all available space. uintptr_t availableSize = heap_getAllocationSize(newAllocation.header); heap_setUsedSize(newAllocation.header, availableSize); // Move the data from the old allocation to the new allocation. if (std::is_move_constructible::value) { // If T is move constructible, we do not have to clone the elements. for (intptr_t e = 0; e < this->impl_buffer_length; e++) { new (newElements + e) T(std::move(this->impl_elements[e])); } } else { // If T is not move constructible, we have to create a copy and then destroy the original. for (intptr_t e = 0; e < this->impl_buffer_length; e++) { new (newElements + e) T(this->impl_elements[e]); this->impl_elements[e].~T(); } } // Transfer ownership to the new allocation. heap_decreaseUseCount(this->impl_elements); this->impl_elements = newElements; this->impl_buffer_length = availableSize / sizeof(T); } } void impl_setLength(intptr_t newLength) { if (newLength < this->impl_length) { for (intptr_t e = newLength; e < this->impl_length; e++) { // In-place descruction. this->impl_elements[e].~T(); } } else { this->impl_reserve(newLength); } this->impl_length = newLength; } template void impl_setElements(ARGS&&... args) { intptr_t elementCount = sizeof...(args); this->impl_reserve(elementCount); this->impl_length = elementCount; std::initializer_list otherArguments = { T(args)... }; for (intptr_t e = 0; e < elementCount; e++) { new (this->impl_elements + e) T(otherArguments.begin()[e]); } } public: // Copy constructor List(const List& source) { this->impl_setLength(source.length()); for (intptr_t e = 0; e < source.length(); e++) { new (this->impl_elements + e) T(source.impl_elements[e]); } } // Move constructor List(List&& source) noexcept { // No pre-existing content when move constructing. this->impl_elements = source.impl_elements; this->impl_length = source.impl_length; this->impl_buffer_length = source.impl_buffer_length; source.impl_elements = nullptr; source.impl_length = 0; source.impl_buffer_length = 0; } // Copy assignment operator List& operator = (const List& source) { if (this != &source) { // Delete any pre-existing content. this->impl_setLength(0); // Clone the content. this->impl_setLength(source.length()); for (intptr_t e = 0; e < source.length(); e++) { new (this->impl_elements + e) T(source.impl_elements[e]); } } return *this; } // Move assignment operator List& operator = (List&& source) { if (this != &source) { // Delete any pre-existing content when move assigning. this->impl_setLength(0); heap_decreaseUseCount(this->impl_elements); // Move the content. this->impl_elements = source.impl_elements; this->impl_length = source.impl_length; this->impl_buffer_length = source.impl_buffer_length; source.impl_elements = nullptr; source.impl_length = 0; source.impl_buffer_length = 0; } return *this; } // Constructors List() {} template< typename FIRST, typename... OTHERS, DSR_ENABLE_IF(DSR_CONVERTIBLE_TO(FIRST, T)) > List(FIRST first, OTHERS... others) { this->impl_setElements(first, others...); } ~List() { // Destroy all elements. this->impl_setLength(0); // Free the allocation. heap_decreaseUseCount(this->impl_elements); this->impl_elements = nullptr; this->impl_buffer_length = 0; } // Post-condition: Returns the element at index from the range 0..length-1. T& operator[] (intptr_t index) { impl_baseZeroBoundCheck(index, this->impl_length, "List index"); return this->impl_elements[index]; } const T& operator[] (intptr_t index) const { impl_baseZeroBoundCheck(index, this->impl_length, "List index"); return this->impl_elements[index]; } // Post-condition: Returns the number of elements in the array list. inline intptr_t length() const { return this->impl_length; } // Side-effect: Makes sure that the buffer have room for at least minimumLength elements. // Warning! Reallocation may invalidate old pointers and references to elements in the replaced buffer. void reserve(intptr_t minimumLength) { impl_reserve(minimumLength); } // Post-condition: Returns an index to the first element, which is always zero. // Can be used for improving readability when used together with lastIndex. intptr_t firstIndex() const { return 0; } // Post-condition: Returns an index to the last element. intptr_t lastIndex() const { return this->impl_length - 1; } // Post-condition: Returns a reference to the first element. T& first() { impl_nonZeroLengthCheck(this->impl_length, "Length"); return this->impl_elements[0]; } // Post-condition: Returns a reference to the first element. const T& first() const { impl_nonZeroLengthCheck(this->impl_length, "Length"); return this->impl_elements[0]; } // Post-condition: Returns a reference to the last element. T& last() { impl_nonZeroLengthCheck(this->impl_length, "Length"); return this->impl_elements[this->lastIndex()]; } // Post-condition: Returns a reference to the last element. const T& last() const { impl_nonZeroLengthCheck(this->impl_length, "Length"); return this->impl_elements[this->lastIndex()]; } // Side-effect: Removes all elements by setting the count to zero. void clear() { this->impl_setLength(0); } // Side-effect: Swap the order of two elements. // Useful for moving and sorting elements. void swap(intptr_t indexA, intptr_t indexB) { impl_baseZeroBoundCheck(indexA, this->impl_length, "Swap index A"); impl_baseZeroBoundCheck(indexB, this->impl_length, "Swap index B"); std::swap(this->impl_elements[indexA], this->impl_elements[indexB]); } T& push(const T& newValue) { this->impl_setLength(this->impl_length + 1); // Copy construction. new (&(this->last())) T(newValue); return this->last(); } // Side-effect: Pushes a new element at the end. // Warning! Reallocation may invalidate old pointers and references to elements in the replaced buffer. // Post-condition: Returns an index to the new element in the list. intptr_t pushGetIndex(const T& newValue) { this->impl_setLength(this->impl_length + 1); // Copy construction. new (&(this->last())) T(newValue); return this->lastIndex(); } // Side-effect: Pushes a new element constructed using the given arguments. // Warning! Reallocation may invalidate old pointers and references to elements in the replaced buffer. // Post-condition: Returns a reference to the new element in the list. template T& pushConstruct(ARGS&&... args) { this->impl_setLength(this->impl_length + 1); // Copy construction. new (&(this->last())) T(std::forward(args)...); return this->last(); } // Side-effect: Pushes a new element constructed using the given arguments. // Warning! Reallocation may invalidate old pointers and references to elements in the replaced buffer. // Post-condition: Returns an index to the new element in the list. template intptr_t pushConstructGetIndex(ARGS&&... args) { this->impl_setLength(this->impl_length + 1); new (&(this->last())) T(std::forward(args)...); return this->lastIndex(); } // TODO: Implement constant time remove with swap for not preserving any order. // Side-effect: Deletes the element at removedIndex without changing the order of other elements. // Pre-condition: Returns true on success and false on failure. bool remove(intptr_t removedIndex) { if (0 <= removedIndex && removedIndex < this->impl_length) { // Shift all following elements without cloning, which will call the destructor for the removed element. for (intptr_t e = removedIndex; e < this->impl_length - 1; e++) { // Move assignment. this->impl_elements[e] = std::move(this->impl_elements[e + 1]); } this->impl_length--; return true; } else { return false; } } // Side-effect: Deletes the last element. // Pre-condition: Returns true on success and false on failure. bool pop() { if (this->impl_length > 0) { //impl_elements[this->impl_length - 1].~T(); this->impl_setLength(this->impl_length - 1); return true; } else { return false; } } }; } #endif