list
and forward_list
. I understand that list
supports bidirectional iteration, while forward_list
supports only forward iteration. One thing that really throws me off is about ranges. I understand that both list
and vector
support half-open ranges: forward_list
s have a method called before_begin() so that they support fully open ranges? list
s provide many methods like splice() and insert(). But why do all forward_list
s have special methods suffixed with a "_after"? Ex: splice_after() and insert_after()
|
|
|
|
Since singly linked lists have strictly less functionality than doubly linked lists, the only reason to include them in the library is performance. It's important to notice, furthermore, that the performance difference isn't a matter of asymptotic complexity, but a matter of bytes and cycles. Inserting a new node is O(1) in either a singly linked list or a doubly linked list, but in a singly linked list it's faster. This fact influences most of the design decisions in this proposal. The main goal of this proposal is that forward_list should have zero space or time overhead relative to a hand-written C-style singly linked list. ... The main disadvantage of this design is that programmers have to use insert_after and erase_after instead of the more familiar insert and erase. The advantage, however, is that all other operations are more efficient: iterator dereference is a single pointer dereference, and the list object itself doesn't need to use a second word to store a tail pointer. Admittedly these performance issues are small: they involve changes in small constant factors, not changes of asymptotic complexity. I still consider them important, however, because those constant factors are the only reason for using a singly linked list instead of a doubly linked list the first place. The main design goal for this proposal is that forward_list should have zero overhead relative to a hand-written C-style linked list like xmlEnumeration, and a design in which iterators point to the preceding list node does not satisfy that goal. Long experience shows that for some uses the restrictions of C-style or lisp-style singly linked lists are not onerous. For other uses, std::list is still available. - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2543.htm |
I emphasize the importance of cache effects. In my experience, all but true experts tend to forget those when algorithms are discussed. And, yes, my recommendation is to use std::vector by default. More generally, use a contiguous representation unless there is a good reason not to. Like C, C++ is designed to do that by default. - Stroustrup http://www.stroustrup.com/bs_faq.html#list |
Consider a simple example: generate N random integers and insert them into a sequence so that each is in its proper position in the numerical order. For example, if the random numbers are 5, 1, 4, and 2, the sequence should grow like this: 5 1 5 1 4 5 1 2 4 5 Once the N elements are in order, we remove them one at a time by selecting a random position in the sequence and removing the element there. For example, if we choose positions 1, 2, 0, and 0 (using 0 as the origin), the sequence should shrink like this: 1 2 4 5 1 4 5 1 4 4 Now, for which N is it better to use a linked list than a vector (or an array) to represent the sequence? If we naively apply complexity theory, that answer will be something like, “Is this a trick question? A list, of course!” We can insert an element into and delete from a linked list without moving other elements. In a vector, every element after the position of an inserted or deleted element must be moved. Worse still, if you don’t know the maximum number of elements in advance, it’s occasionally necessary to copy the entire vector to make room for another element. Depending on the machine architecture and the programming language, the answer will be that the vector is best for small to medium values of N. When I ran the experiment on my 8-Gbyte laptop, I found N to be much larger than 500,000. ... In an attempt at fairness, I didn’t use a binary search to speed up insertion into the vector. Nor did I use random access to find the deletion point in the vector version. This keeps the number of elements traversed the same for all versions. In fact, I used identical generic code for the vector and the lists. Is this an academic curiosity? No. Infrastructure application developers tell me that compactness and predictable access patterns are essential for efficiency. - Stroustrup http://www.stroustrup.com/Software-for-infrastructure.pdf |
|
|
echo '======== clang++ ========' && clang++ -std=c++14 -stdlib=libc++ -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out echo '========== g++ ==========' && g++ -std=c++14 -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out ======== clang++ ======== vector: 1330 millisecs list: 3630 millisecs ========== g++ ========== vector: 780 millisecs list: 4110 millisecs |
echo '======== clang++ ========' && clang++ -std=c++14 -stdlib=libc++ -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out echo '========== g++ ==========' && g++ -std=c++14 -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out ======== clang++ ======== vector: 3070 millisecs list: 3120 millisecs ========== g++ ========== vector: 2570 millisecs list: 3390 millisecs |