All STL containers require to provide an end()
sentinel. This means that, if bidirectional iteration is supported, there must be a valid past-the-end position on which a decrement operation can be performed through the iterator interface. In this part, only node-based containers that allow bidirectional iteration will be treated, and therefore the std::forward_list
container will be momentarily ignored.
If the end()
sentinel were simply represented by any sentinel value, such as a null pointer, it would not be possible to perform the aforementioned decrement operation in order to move to the previous node. One way to solve this issue is the use of a "dummy node".
In the libstdc++ and libc++ implementations, this node is statically allocated in order that it is present in the container throughout the lifetime of the object and allows for a valid past-the-end position even if the sequence is completely empty. It is important to note that the word "sequence" is used to refer to the in-order iteration of all values in the container, regardless of whether the nodes are arranged linearly or structured in a more complex graph (std::set
).
If the result were achieved by allocating a complete node, this would result in two side effects:
- the space that is occupied by the dummy node would not be limited by the number of pointers, which are the only member attributes required to allow linkage with other nodes, but would be proportional to the type
T
size;
- if the type
T
is stored directly in the node, when the object is constructed, a method should be found to avoid the T
object construction.
The last issue can be solved by replacing the type T
with an aligned storage, but this would not solve the issue of occupied space. This situation makes it necessary to introduce a specific structure for the dummy node, which contain only pointers. However, the creation of such a structure would lead to an inter-operability problem because a pointer to node can not be converted to a pointer to dummy node and vice versa.
A solution consists of deriving the complete node from the dummy node, to which the data attribute, or an aligned storage, is added.
Example:
struct _List_node_base
{
_List_node_base* _M_next;
_List_node_base* _M_prev;
};
template <typename _Tp>
struct _List_node : public _List_node_base
{
_Tp _M_data;
};
Similar reasoning can be applied to the std::forward_list
container, which, however, requires a before_begin()
sentinel on which an increment operation can be performed through the iterator interface.
std::list
and libc++,__list_node_base
class does not seem to be used anywhere else than just as a base class of__list_node
. I guess the reason is mainly modularity here, that is, the single-entity/class-single-responsibility principle. Node base takes care about linking of nodes to other nodes, while the node class itself takes care about stored values. – Overdose__list_iterator
has one data member:__ptr_
which is of type__link_pointer
, itself ultimately an instantiation of the__list_node_base
template. – Kerley__list_node_base
are those (base) subobjects of_list_node
.__link_pointer
is a pointer. – Overdosestatic_cast
s. Splitting something likesl_full_node
(above) seems a very granular approach - surely there is also a performance benefit? – Kerley