Class templates in C++ can specialized for particular combination of template arguments. There a quite a few reasons why the author may choose to specialize a class template including, fixing incorrect behaviour for a particular type argument, optimizing for a type argument, and others.
In this article we will show both types of specialization, explicit and partial and look at an example of using this with a data structure and explore how specialization allow use to provide traits, allowing other developers to get meta data like inforation of types.
The data structure we will use is a fixed size stack and show how to specialize for pointer types and void. We will also develop traits for is_const, is_pointer, remove_reference and is_void.
Lets look at the fixed_stack<T, N> data structure first.
s
So what are the issues with the above approach? It works for all arrays right? Well it does work for all arrays of T, but what if we want to find a value in a std::vector? Lets look at that?
Well that works right? its so close to the array example though, just a few minor changes. What if we wanted to find a value in a std::string? Well I think you are starting to get the point here. How can we generalize this algorithm to work for any data structure? Enter iterators.
Iterators are pointer like constructs that allow you traverse a data structure, much like traversing an array. Algorithms are written in terms of iterators and data structures provide the algorithms iterators into their data structure to allow the traversal. Lets look a really basic example. Lets implement this with our own cut down version of std::array<T, N>.
Running the program gives the output:
Lets look at the array<T, N> class. Firstly it has a bunch of type alias at the top. These type aliases are expected of containers that are STL compliant. The 2 we are interested in though are that of iterator and const_iterator. Firstly for the array<T, N> these are nothing but alias to pointer and const pointer types, which makes sense for a type that models an array<T, N>. For more complicated classes which we will look at later in this article, the iterator type aliases wont be merly pointer aliases, but aliases to user defined types.
The next thing to notice is the begin / cbegin and end / cend functions. These are also required as these are used to provide the start and end of the iterator range. Remember an iterator provides access to the internals of a datastructure and allows us to traverse it. An important point to make is that the STL is modelled on half open intervals. If you do not know what this we will go over the concept now to fill you in.
Intervals
Ok what are intervals and what is this notation. An iterval is a range of numbers. The notation allows us to specify whether the end numbers are included in the range. Lets look at some examples.
So how does this relate to the STL. Well the range of elements in a data structure that a pair of iterators, one for the start and one for the end is [begin, end). So this means that the end is one past the end. If you look at what end() returns in the array<T, N> returns it is one past the end. There are a bunch of benefits for using one past the end which we will duscuss later on. You just need to remember this for now.
Back to the code
In the previous example we passed the results of begin() and end() to the function std::for_each(). This algorithm works on a pair of iterators. For each element pointed to by the iterator, the call back is called, printing the element to the console.
Lets get a feel for this by implementing std::for_each ourselves.
std::for_each takes a pair of iterators, first and last, of type InputIt and a Callback function f of type F. You can see that this function requires that the iterators support !=, ++ and the deference operator *. This is not a problem for out simple array<T, N> which just uses a pointer type as iterators, but how about a linked list? It can not be modelling a simply a pointer type. Lets have a look at the requirements of iterator types.
Concepts
A concept is a set of requirements on a type. Both syntactic, what expressions should be allowed, and semantic requirements, what should happen as a result of the expressions. A type that satisfies the requirement of a concept is said to model the concept. A refinement is a concept that adds additonal requirements to an existing concept.
Some of the basic concepts from the STL are:
Iterator Concepts
Iterators in the STL also form a heirarchy of concepts. Iterators the support being able to read from and moved forwards only once are input iterators. The reason for being restricted to be read from only once is the iterator might be something like a network stream, which by nature can be consumed only once. Lets examine Input Iterators in more details to see the operations which must be supported.
As an example of a more sophisticated data structure, lets look at a linked list that can be traversed in a single direction, forwards. The forward_list class, defines two iterator classes, forward_list_iterator and const_forward_list_iterator. Lets take a look at the code.
The first thing we can observe it we define the iterators dereference operator, operator*() in terms of returning a reference to the value field of node structure. Secondly the arrow operator, operator->() is defined in terms of returning a pointer to the value field of node structure. This is important as when an algorithm, such as std::find need a value from the iterator we return the correct value for use in the algorithm. Also the prefix operator, operator++() is defined in terms of updating the the ptr to the current node to node->next. This is the logically way for the iterator of a list class to move through its collection of nodes. Finally we define equality, operator==() and inequality, operator!=() in terms of comparing node pointers. This again makes sense as we stop when we hit the end iterators address, which is nullptr, which matches how we would do this if we just wrote an algorithm to walk a list.
The iterators also define a typedef iterator_category. This is a requirement for iterators. This lets algorithms know what the iterator supports in terms of requirements. Algorithms can be customised to use more efficent algorithms if the iterator supports them. For example std::random_access_iterator_tag supports more operators then std::forward_iterator_tag.
So this wraps up our introduction into iterators. I hope this helps. In the following article on iterators we will discuss iterator_traits. But before then there will be additional articles on the prerequisites such as template specialization and traits.
Until next time.
C++, Templates, STL, Specialization
I feedback.
Let me know what you think of this article in the comment section below!