2

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:

  1. 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?)
  2. 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?

1f604
  • 245
  • 1
  • 7

0 Answers0