I'd like to use std::forward_list
Because:
Forward list is a container which supports fast insertion and removal of elements from anywhere from the container
But there's no *std::forward_list::push_back* implementation.
Is there a high-performance way to add support for the one or no reason to do it?
I recommend against
std::forward_list
just like I recommend againststd::list
in almost all situations. Personally, I've never found a situation in my code where a linked list was the best data structure.In C++, your default go-to data collection should be
std::vector
. It gives you efficientpush_back
, if that is what you really need. It technically does not give you efficient deletion and insertion from the middle if you only look at abstract big-O complexity measurements of that one operation. In the real world, however,std::vector
still wins even for inserting and deleting in the middle.As an example, Bjarne Stroustrup created a test of a 100,000 element
std::list
vs.std::vector
. He would search each for an element and delete it. Then he would find an insertion point and insert into the middle. He could have use a binary search on thestd::vector
, but did not to make the comparison 'more fair'.The results show a strong win for
std::vector
, even in this situation wherestd::list
is supposed to be strong. Simply traversing thestd::list
takes so much longer because of how far apart in memory all of the objects are.std::list
is not cache-friendly, which is possibly the most important thing for modern processors.The complete talk by Bjarne Stroustrup
Thorough explanation of the effects, with benchmarks at multiple sizes
Note that this second link here gives some situations of where you may possibly want to use a
std::list
, such as when the size of the elements is large. However, I've been in a situation where I have many elements in a particular order and needed to delete some.These elements were larger than any built-in type, but not huge, perhaps 20-30 bytes each on a 32-bit computer). The number of elements was large enough so that my entire data structure was a few hundred MiB. The data collection was a set of values that could theoretically be a valid based on currently known information. The algorithm iterated over all elements and removed elements that could no longer be valid based on new information, with each pass probably deleting somewhere around 80% of the remaining elements.
My first implementation was a straightforward
std::vector
approach where I deleted invalid elements as I traversed. This worked for small test data sets, but when I tried to do the real thing, it was too slow to be useful. I switched to astd::list
as the container, but used the same algorithm, and I saw significant performance improvements. However, it was still too slow to be useful. The winning change was to switch back to astd::vector
, but instead of deleting elements in place that were bad, I created a newstd::vector
, and any elements I found that were good were put into thatstd::vector
, and then at the end of the function I would simply discard the oldstd::vector
and use the new one, and this gave me about as much of a speed up over thestd::list
as thestd::list
gave me over my originalstd::vector
implementation, and this was just fast enough to be useful.std::forward_list
supports fast insertion and removal, but not traversal to the end. To implement.push_back
, you'll first need to get to the end of the list, which is O(N) and not fast at all, which is probably why it's not implemented.You could find the iterator to the last element by incrementing
.before_begin
N timesand then use
.insert_after
or.emplace_after
to insert the element:Unfortunatelly I can't add a comment (low reputation) but I just wanted to mention that one of the forward_list & list advantages is that insert-delete operations do not invalidate iterators. I had an application where collection of elements was growing while iterating and processing individual elements. Absence of iterator invalidation allowed me to implement segment scan (begin of the list as start of the segment and begin of the last segment as end).
There is no
push_back
because the list doesn't keep track of the back of the list, only the front.You could write a wrapper around the list that maintains an iterator to the last element, and implements
push_back
using eitherinsert_after
orpush_front
depending on whether the list is empty. This will get rather complicated if you want to support the more complex operations (e.g.sort
andsplice_after
).Alternatively, if you don't need
push_back
to be fast, it's straightforward to do it in linear time.Unless memory is extremely tight, the best solution is to use
list
. This has the same performance characteristics asforward_list
, and is bidirectional, supportingpush_back
as well aspush_front
; the cost is an extra pointer per element.The point of std::forward_list is to be an ultra-stripped-down version of std::list, so it doesn't store an iterator to the last element. If you need one, you'll have to maintain it yourself, like so: