#ifndef VTR_SMALL_VECTOR #define VTR_SMALL_VECTOR #include #include #include #include #include "vtr_assert.h" namespace vtr { namespace small_vector_impl { //The long format view of the vector, which consists of a dynamically allocated array, //capacity and size. template struct long_format { T* data_ = nullptr; S capacity_ = 0; S size_ = 0; }; //The short format view of the vector, which consists of an in-place (potentially empty) //array of objects, a pad, and a size. template struct short_format { std::array data_; std::array pad_; //Padding to keep size_ aligned in both long_format and short_format S size_ = 0; }; //We need a specialized version of short_format for padding of size zero, //since a std::array with zero array size may still have non-zero sizeof() template struct short_format { std::array data_; S size_ = 0; }; } // namespace small_vector_impl //vtr::small_vector is a std::vector like container which: // * consumes less memory: sizeof(vtr::small_vector) < sizeof(std::vector) // * possibly stores elements in-place (i.e. within the object) // //On a typical LP64 system a vtr::small_vector consumes 16 bytes by default and supports //vectors up to ~2^32 elements long, while a std::vector consumes 24 bytes and supports up //to ~2^64 elements. The type used to store the size and capacity is configurable, //and set by the second template parameter argument. Setting it to size_t will replicate //std::vector's characteristics. // //For short vectors vtr::small_vector will try to store elements in-place (i.e. within the //vtr::small_vector object) instead of dynamically allocating an array (by re-using the //internal storage for the pointer, size and capacity). Whether this is possible depends on //the size and alignment requirements of the value type, as compared to //vtr::small_vector. If in-place storage is not possible (e.g. due to a large value //type, or a large number of elements) a dynamic buffer is allocated (similar to //std::vector). // //This is a highly specialized container. Unless you have specifically measured it's //usefulness you should use std::vector. template class small_vector { public: //Types typedef T value_type; //typedef allocator_type //Allocator, unimplemented typedef value_type& reference; typedef const value_type& const_reference; typedef value_type* pointer; typedef const value_type* const_pointer; typedef T* iterator; typedef const T* const_iterator; typedef std::reverse_iterator reverse_iterator; typedef std::reverse_iterator const_reverse_iterator; typedef ptrdiff_t difference_type; typedef S size_type; public: //Constructors small_vector() { if (SHORT_CAPACITY == 0) { long_.data_ = nullptr; long_.capacity_ = 0; } set_size(0); } small_vector(size_type nelem) : small_vector() { reserve(nelem); for (size_type i = 0; i < nelem; i++) { emplace_back(); } set_size(0); } public: //Accessors const_iterator begin() const { return cbegin(); } const_iterator end() const { return cend(); } const_reverse_iterator rbegin() const { return crbegin(); } const_reverse_iterator rend() const { return crend(); } const_iterator cbegin() const { if (is_short()) { return short_.data_.data(); } return long_.data_; } const_iterator cend() const { if (is_short()) { return short_.data_.data() + size(); } return long_.data_ + size(); } const_reverse_iterator crbegin() const { return const_reverse_iterator(cend()); } const_reverse_iterator crend() const { return const_reverse_iterator(cbegin()); } size_type size() const { //Padding ensures long/short format sizes are always aligned return long_.size_; } size_t max_size() const { return std::numeric_limits::max(); } size_type capacity() const { if (is_short()) { return SHORT_CAPACITY; //Fixed capacity } return long_.capacity_; } bool empty() const { return size() == 0; } const_reference operator[](size_t i) const { if (is_short()) { return short_.data_[i]; } return long_.data_[i]; } const_reference at(size_t i) const { if (i > size()) { throw std::out_of_range("Index out of bounds"); } return operator[](i); } const_reference front() const { return *begin(); } const_reference back() const { return *(end() - 1); } const_pointer data() const { if (is_short()) { short_.data_; } return long_.data_; } public: //Mutators iterator begin() { //Call const method and cast-away constness return const_cast(const_cast*>(this)->begin()); } iterator end() { return const_cast(const_cast*>(this)->end()); } reverse_iterator rbegin() { //Call const method and cast-away constness return reverse_iterator(const_cast*>(this)->end()); } reverse_iterator rend() { return reverse_iterator(const_cast*>(this)->begin()); } void resize(size_type n) { resize(n, value_type()); } void resize(size_type n, value_type val) { if (n < size()) { //Remove at end erase(begin() + n, end()); } else if (n > size()) { //Insert new elements at end insert(end(), n - size(), val); } } void reserve(size_type num_elems) { //Don't change capacity unless requested number of elements is both: // * More than the short capacity (no need to reserve up to short capacity) // * Greater than the current size (capacity can never be below size) if (num_elems > SHORT_CAPACITY && num_elems > size()) { change_capacity(num_elems); } } void shrink_to_fit() { if (!is_short()) { change_capacity(size()); } } reference operator[](size_t i) { return const_cast(const_cast*>(this)->operator[](i)); } reference at(size_t i) { return const_cast(const_cast*>(this)->at(i)); } reference front() { return const_cast(const_cast*>(this)->front()); } reference back() { return const_cast(const_cast*>(this)->back()); } pointer data() { return const_cast(const_cast*>(this)->data()); } template void assign(InputIterator first, InputIterator last) { insert(begin(), first, last); } void assign(size_type n, const value_type& val) { insert(begin(), n, val); } void assign(std::initializer_list il) { assign(il.begin(), il.end()); } void push_back(value_type value) { //Construct default value_type at new location auto new_ptr = next_back(); new (new_ptr) T(); //Since we took a copy in the argument, we can move it //into the new location *new_ptr = std::move(value); } void pop_back() { if (size() > 0) { erase(end() - 1); } } iterator insert(const_iterator position, const value_type& val) { return insert(position, 1, val); } iterator insert(const_iterator position, size_type n, const value_type& val) { //Location of position as an index, which will be //unchanged if the underlying storage is reallocated size_type i = std::distance(cbegin(), position); //If needed, grow capacity // //Note that change_capacity will automatically convert from short to long //format if required. size_type new_size = size() + n; if (capacity() < new_size) { change_capacity(new_size); } iterator first = begin() + i; iterator last = first + n; reverse_swap_elements(first, end(), end() + n - 1); //Insert new values at end std::uninitialized_fill(first, last, val); set_size(new_size); return first; } iterator insert(const_iterator position, size_type n, value_type&& val) { return insert(position, n, value_type(val)); //TODO: optimize for moved val } //Range insert //template //iterator insert(const_iterator position, InputIterator first, InputIterator last) { ////Location of position as an index, which will be ////unchanged if the underlying storage is reallocated //size_type i = std::distance(cbegin(), position); //size_type n = std::distance(first, last); ////If needed, grow capacity //// ////Note that change_capacity will automatically convert from short to long ////format if required. //size_type new_size = size() + n; //if (capacity() < new_size) { //change_capacity(new_size); //} //reverse_swap_elements(begin() + i, end(), end() + n - 1); ////Insert new values at end //std::uninitialized_copy(first, last, begin() + i); //set_size(new_size); //return begin() + i; //} iterator erase(const_iterator position) { return erase(position, position + 1); } iterator erase(const_iterator first, const_iterator last) { //Number of elements to erase size_type n = std::distance(first, last); //Location of position as an index, which will be //unchanged if the underlying storage is changed size_type i_first = std::distance(cbegin(), first); size_type new_size = size() - n; const_iterator position = first; if (!is_short() && new_size <= SHORT_CAPACITY) { //Convert from long format to short/in-place format //Keep handle on buffer and original size auto buff_ptr = long_.data_; size_type orig_size = size(); //Copy into in-place the valid (not-to-be-erased) values in //[begin, first) and [last, end) // //Note that we can use uninitialized_copy since the long format //has only basic data types, which have no destructors to call auto buff_begin = buff_ptr; auto buff_end = buff_begin + orig_size; auto erase_begin = buff_ptr + i_first; auto erase_end = erase_begin + n; //Copy from beginning until start of erase auto inplace_ptr = short_.data_.data(); for (auto buff_itr = buff_begin; buff_itr != erase_begin; ++buff_itr) { new (inplace_ptr++) T(*buff_itr); } //Copy from end of erase until end of buf for (auto buff_itr = erase_end; buff_itr != buff_end; ++buff_itr) { new (inplace_ptr++) T(*buff_itr); } VTR_ASSERT_SAFE(std::distance(short_.data_.data(), inplace_ptr) == new_size); //Clean-up elements in buffer and free it destruct_elements(buff_begin, buff_end); dealloc(buff_ptr); //New position position = begin() + i_first; } else { //Remove elements in either long or short formats iterator first2 = begin() + i_first; iterator last2 = first2 + n; //Swap all elements in [first, last) to the end. //That is with those within [last, end()) if (last2 < end()) { swap_elements(last2, end(), first2); } //Finally destruct the elements in [last, end()); that is the //elements which were originally to be erased destruct_elements(end() - n, end()); //Note that capacity is unchanged, so we do not need to change //position in this case } //Shrink size set_size(new_size); return begin() + std::distance(cbegin(), position); } void swap(small_vector& other) { swap(*this, other); } friend void swap(small_vector& lhs, small_vector& rhs) { using std::swap; if (lhs.is_short() && rhs.is_short()) { //Both short std::swap(lhs.short_, rhs.short_); } else if (!lhs.is_short() && !rhs.is_short()) { //Both long std::swap(lhs.long_, rhs.long_); } else { //Mixed long/short VTR_ASSERT_SAFE(lhs.is_short() != rhs.is_short()); auto& long_vec = ((lhs.is_short()) ? rhs : lhs); auto& short_vec = ((lhs.is_short()) ? lhs : rhs); //If the two vectors are in different formats we can't just swap them, //since the short format has real values (potentially with destructors), //while the long format has only basic data types. // //Instead we copy the short_vec values into long, destruct the original short_vec //values and then set short_vec to point to long_vec's original buffer (avoids //extra copy of long elements). //Save long data pointer long_buf = long_vec.long_.data_; size_type long_size = long_vec.long_size_; size_type long_capacity = long_vec.long_.capacity_; //Copy short data into long // //Note that the long format contains only basic data types with no destructors to call, //so we can use uninitialzed copy std::uninitialized_copy(short_vec.short_.begin(), short_vec.short_.end(), long_vec.short_.data_); long_vec.short_.size_ = short_vec.size(); //Destroy original elements in short short_vec.destruct_elements(); //Copy long data into short short_vec.long_.data = long_buf; short_vec.long_.capacity_ = long_capacity; short_vec.long_.size_ = long_size; } } void clear() { //Destruct all elements and clear size, but do not free memory destruct_elements(); set_size(0); } template void emplace_back(Args&&... args) { //Construct in-place new (next_back()) T(std::forward(args)...); } //Unsupported: Emplace at position //template //void emplace(const_iterator position, Args&&... args) { //throw std::logic_error("unimplemented"); //} public: //Comparisons friend bool operator==(const small_vector& lhs, const small_vector& rhs) { if (lhs.size() != rhs.size()) { return false; } return std::equal(lhs.begin(), lhs.end(), rhs.begin()); } friend bool operator<(const small_vector& lhs, const small_vector& rhs) { return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); } friend bool operator!=(const small_vector& lhs, const small_vector& rhs) { return !(lhs == rhs); } friend bool operator>(const small_vector& lhs, const small_vector& rhs) { return rhs < lhs; } friend bool operator<=(const small_vector& lhs, const small_vector& rhs) { return !(rhs < lhs); } friend bool operator>=(const small_vector& lhs, const small_vector& rhs) { return !(lhs < rhs); } public: //Lifetime management ~small_vector() { destruct_elements(); if (!is_short()) { dealloc(long_.data_); } } small_vector(const small_vector& other) { if (other.is_short()) { ~small_vector(); //Clean-up elements //Copy in place short_ = other.short_; } else { if (!is_short() && capacity() >= other.size()) { //Re-use existing buffer, since it has sufficient capacity destruct_elements(); } else { ~small_vector(); //Clean-up elements, potentially freeing buffer //Create new buffer of exact size long_.data_ = alloc(other.size()); long_.capacity_ = other.size(); } set_size(other.size()); //Copy elements std::uninitialized_copy(other.begin(), other.end(), long_.data_); } } small_vector(small_vector&& other) : small_vector() { swap(*this, other); //Copy-swap } small_vector& operator=(small_vector other) { swap(*this, other); //Copy-swap return *this; } private: //Internal types static constexpr size_t LONG_FMT_SIZE = sizeof(small_vector_impl::long_format); static constexpr size_t LONG_FMT_ALIGN = alignof(small_vector_impl::long_format); //The number of value types which can be stored in-place in the object (may be zero) static constexpr size_t SHORT_CAPACITY = (LONG_FMT_SIZE - sizeof(size_type)) / sizeof(value_type); //The amount of padding required to ensure the size_ attributes of long_format and short_format //are aligned. static constexpr size_t SHORT_PAD = LONG_FMT_SIZE - (sizeof(value_type) * SHORT_CAPACITY + sizeof(size_type)); static constexpr size_t SHORT_FMT_SIZE = sizeof(small_vector_impl::short_format); static constexpr size_t SHORT_FMT_ALIGN = alignof(small_vector_impl::short_format); static_assert(LONG_FMT_SIZE == SHORT_FMT_SIZE, "Short and long data formats must have same size"); static_assert(LONG_FMT_ALIGN % SHORT_FMT_ALIGN == 0, "Short and long data formats must have compatible alignment"); public: static constexpr size_t INPLACE_CAPACITY = SHORT_CAPACITY; private: //Internal methods //Returns a pointer to the (uninitialized) location for the next element to be added. //Automatically grows the storage if needed. T* next_back() { T* next = nullptr; if (size() < SHORT_CAPACITY) { //Space in-place next = short_.data_.data() + size(); } else { //Dynamically allocated if (size() == capacity()) { //Out of space grow(); } next = long_.data_ + size(); } ++long_.size_; VTR_ASSERT_SAFE(size() <= capacity()); return next; } //Increases the capacity by GROWTH_FACTOR // //Note that this automatically handles the case of growing beyond SHORT_CAPACITY and //switching to long_format void grow() { //How much to scale the size of the storage when out of space constexpr size_type GROWTH_FACTOR = 2; VTR_ASSERT_SAFE_MSG(size() >= SHORT_CAPACITY, "Should only grow capacity when at or beyond SHORT_CAPACITY"); VTR_ASSERT_SAFE_MSG(capacity() <= (max_size() / GROWTH_FACTOR), "No capacity overflow"); size_type new_capacity = std::max(1, capacity() * GROWTH_FACTOR); //TODO: Consider ensuring new_capacity is always a power of 2, may be easier on the memory allocator... VTR_ASSERT_SAFE_MSG(new_capacity > capacity(), "Grown capacity should be greater than previous capacity"); change_capacity(new_capacity); } //Changes capacity to new_capacity // //It is assumed that new_capacity is > SHORT_CAPACITY. // //If currently in short format, automatically converts to long format void change_capacity(size_type new_capacity) { VTR_ASSERT_SAFE_MSG(new_capacity >= size(), "New capacity should be at least size"); if (new_capacity == capacity()) { return; //Already at correct capacity } //Get new raw memory T* tmp_data = alloc(new_capacity); //Copy values std::uninitialized_copy(begin(), end(), tmp_data); //Clean-up the old values //We do this before updating the array pointer, since if we are updating //from short to long the assignment would corrupt the old values destruct_elements(); //Update std::swap(long_.data_, tmp_data); long_.capacity_ = new_capacity; //Free memory if we aren't using the inplace buffer if (!is_short()) { dealloc(tmp_data); } } //Returns true if using the short/in-place format bool is_short() const { return SHORT_CAPACITY > 0u //Can use the inplace buffer && size() <= SHORT_CAPACITY; //Not using the dynamic buffer } void set_size(size_type new_size) { //The two data (short/long) are padded to //ensure that thier size_ members area always //aligned, allowing is to set the size directly //for both formats short_.size_ = new_size; } //Allocates raw (un-initialzied) memory for nelem objects of type T static T* alloc(size_type nelem) { return static_cast(::operator new(sizeof(T) * nelem)); } //Deallocates a block of memory // //Caller must ensure any object's associated with this block have already had //their destructors called static void dealloc(T* data) { ::operator delete(data); } //Swaps the elements in [src_first, src_last) to positions starting at dst_first // //Returns an iterator to the element in the first swapped location iterator swap_elements(iterator src_first, iterator src_last, iterator dst_first) { VTR_ASSERT_SAFE_MSG(src_first < src_last, "First swap range first must start before last"); auto dst_itr = dst_first; for (auto src_itr = src_first; src_itr != src_last; ++src_itr) { std::swap(*src_itr, *(dst_itr++)); } return src_first; } //Swaps the elements in [src_first, src_last) in reverse order starting at dst_first and working backwards // //Returns an iterator to the element in the first swapped location iterator reverse_swap_elements(iterator src_first, iterator src_last, iterator dst_first) { VTR_ASSERT_SAFE_MSG(src_first < src_last, "First swap range first must start before last"); auto dst_itr = dst_first; for (auto src_itr = src_last - 1; src_itr != src_first - 1; --src_itr) { std::swap(*src_itr, *(dst_itr--)); } return src_first; } //Calls the destructors of all elements currently held void destruct_elements() { destruct_elements(begin(), end()); } void destruct_elements(iterator first, iterator last) { for (auto itr = first; itr != last; ++itr) { itr->~T(); } } void destruct_element(iterator position) { destruct_elements(position, position + 1); } private: //Data //The object data storage is re-used between the long and short formats. // //If the capacity is small (less than or equal to SHORT_CAPACITY) the //short format (which stores element in-place) is used. Otherwise the //long format is used and the elements are stored in a dynamically //allocated buffer union { small_vector_impl::long_format long_; small_vector_impl::short_format short_; }; }; } // namespace vtr #endif