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_buffer
is 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_buffer
immediately doubling in size?) - The current contents are shifted to the middle of the
__split_buffer
. This operation can only happen once before the__split_buffer
must be reallocated.
Is my understanding correct?