I am trying to understand how the STL deque is implemented in libcxx.
As I understand it, the libcxx STL deque is implemented as a vector containing pointers to other vectors. This vector-containing-pointers-to-other-vectors named __map_ and is of type __split_buffer. Now this __split_buffer is clearly not a vector because it has a push_front function which a vector does not have. My question here is: just how efficient is this push_front function? Here is the source code for that push_front function:
template <class _Tp, class _Allocator>
void
__split_buffer<_Tp, _Allocator>::push_front(const_reference __x)
{
if (__begin_ == __first_)
{
if (__end_ < __end_cap())
{
difference_type __d = __end_cap() - __end_;
__d = (__d + 1) / 2;
__begin_ = _VSTD::move_backward(__begin_, __end_, __end_ + __d);
__end_ += __d;
}
else
{
size_type __c = max<size_type>(2 * static_cast<size_t>(__end_cap() - __first_), 1);
__split_buffer<value_type, __alloc_rr&> __t(__c, (__c + 3) / 4, __alloc());
__t.__construct_at_end(move_iterator<pointer>(__begin_),
move_iterator<pointer>(__end_));
_VSTD::swap(__first_, __t.__first_);
_VSTD::swap(__begin_, __t.__begin_);
_VSTD::swap(__end_, __t.__end_);
_VSTD::swap(__end_cap(), __t.__end_cap());
}
}
__alloc_traits::construct(__alloc(), _VSTD::__to_raw_pointer(__begin_-1), __x);
--__begin_;
}
It seems like it uses the move_backward function which according to cppreference the standard implementation simply moves every element in the array which seems to me extremely inefficient. I was assuming that it is using some clever O(1) move_backward implementation however I don't know how to find the implementation that it is using. However I suppose it is possible that it really is using the O(n) implementation and is just calling it only once per n calls to push_front which amortizes the cost to O(1) but leaves the worst-case at still O(n).
In the first case it looks like the entire array is first moved by __d and then __end_ is also incremented by __d? Doesn't this mean that the __end_ variable is incremented by the entirety of the difference between it and the __end_cap()?
In the second case, it looks like it first allocates a __split_buffer of double the capacity of the current __split_buffer, and then it calls the __construct_at_end function which presumably copies the entirety of the current __map_ to the END of the newly allocated __split_buffer, which seems sensible but also surprising. It's sensible because of course you would want spare space for the push_front function to add new elements. But also surprising because why would you add to the end rather than at the middle of the newly allocated __split_buffer function? Wouldn't that immediately result in the newly allocated __split_buffer doubling in size yet again?
All in all my understanding of the function is that a call to push_front when the __map_ has no more spare space in front can result in one of two things happening:
- A new
__split_bufferis allocated which is double the size of the old one and the original contents are copied to the end of that new__split_buffer(which surely must result in that new__split_bufferimmediately doubling in size?) - The current contents are shifted to the middle of the
__split_buffer. This operation can only happen once before the__split_buffermust be reallocated.
Is my understanding correct?