12

I understand how random access iterators work for contiguous containers like std::vector: the iterator simply maintains a pointer to the current element and any additions/subtractions are applied to the pointer.

However, I'm baffled as to how similar functionality could be implemented for a non-contiguous container. My first guess for how std::deque:iterator works, is that it maintains a pointer to some table of the groups of contiguous memory it contains, but I'm not sure.

How would a typical standard library implement this?

JFMR
  • 23,265
  • 4
  • 52
  • 76
chbaker0
  • 1,758
  • 2
  • 13
  • 27
  • Who says a `deque` isn't contiguous? It's usually implemented as a dynamic array. – ooga Apr 22 '14 at 03:16
  • 2
    @ooga from [here](http://en.cppreference.com/w/cpp/container/deque) `As opposed to std::vector, the elements of a deque are not stored contiguously: typical implementations use a sequence of individually allocated fixed-size arrays.` – Bryan Chen Apr 22 '14 at 03:18
  • 1
    @ooga, Then how would it differ from a vector? – chris Apr 22 '14 at 03:18
  • @BryanChen I wonder what that means? A "sequence ... of arrays"? – ooga Apr 22 '14 at 03:20
  • 1
    A diagram for a typical `deque`: http://kremer.cpsc.ucalgary.ca/STL/1024x768/deque.html and an article that looks like it might be helpful (though I've only glanced at it): https://secweb.cs.odu.edu/~zeil/cs361/web/website/Lectures/deques/page/deques.html – Michael Burr Apr 22 '14 at 03:21
  • kind of `std::list>`. i.e. linked list of arrays – Bryan Chen Apr 22 '14 at 03:21
  • @Bryan or like a hash table that has pointers to other tables instead of linked lists. –  Apr 22 '14 at 03:23
  • @MichaelBurr So it has a kind of contiguity. I.e., you can fairly-efficiently randomly access elements. I guess it trys to keep as many contiguous elements as possible. – ooga Apr 22 '14 at 03:23
  • @MichaelBurr That's somewhat like I imagined. So would each iterator hold a reference to `deque`'s map? – chbaker0 Apr 22 '14 at 03:23

2 Answers2

5

You can satisfy the requirememts of a std::deque with a std::vector<std::unique_ptr<std::array<T,N>>> roughly. plus a low/high water mark telling you where the first/last elements are. (for an implementation defined N that could vary with T, and the std::arrays are actually blocks of properly aligned uninitialized memory and not std::arrays, but you get the idea).

Use usual exponential growth, but on both front and back.

Lookup simply does (index+first)/N and %N to find the block and sub element.

This is more expensive than a std::vector lookup, but is O(1).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • At [http://cppreference.com](http://cppreference.com) the page for deque::push_back indicate constant time complexity while the page for vector::push_back indicated amortized constant time complexity. Wouldn't using a vector as a backend for pointers to arrays violate the requirement that deque::push_back be constant? Or is amortized constant acceptable? – chbaker0 Apr 22 '14 at 03:34
  • @mebob elsewhere it states amortized constant: is error. I should fix it after confirming with the standard. – Yakk - Adam Nevraumont Apr 22 '14 at 04:19
  • OK that makes sense. I can't really think of a way to get around the amortized constant limit anyway. And back to my question about iterators: so an iterator would simply hold a reference to the said vector and switch buffers when it reached the end of one? – chbaker0 Apr 22 '14 at 04:28
  • @mebob yes, something like that. – Yakk - Adam Nevraumont Apr 22 '14 at 11:40
  • 1
    You might also want to add that making `N` a compile-time constant that's also a power of two (a `static_assert`, perhaps) should be very good for performance. – Daniel Kamil Kozar Apr 04 '18 at 07:31
3

A deque iterator can be implemented by storing both a pointer to the referenced value and a double pointer to the contiguous block of memory in which that value is located. The double pointer points into a contiguous array of pointers to blocks managed by the deque.

class deque_iterator
{
  T* value;
  T** block;
  …
}

Because both value and block point into contiguous memory, you can implement operations such finding the distance between iterators in constant time (example adapted from libc++).

difference_type operator-(deque_iterator const& x, deque_iterator const& y)
{
  return (x.block - y.block) * block_size
       + (x.value - *x.block)
       - (y.value - *y.block);
}

Note that, while value will not be invalidated by operations such as push_front and push_back, block might be, which is why deque_iterator is invalidated by such operations.

Joseph Thomson
  • 9,888
  • 1
  • 34
  • 38