2.1. Type AssociationsIn C++, the entities that can be manipulated at compile time, called metadata, are divided roughly into two categories: types and non-types. Not coincidentally, all the kinds of metadata can be used as template parameters. The constant integer values used in Chapter 1 are among the non-types, a category that also includes values of nearly everything else that can be known at compile time: the other integral types, enums, pointers and references to functions and "global" objects, and pointers to members.[1]
It's easy to imagine doing calculations on some kinds of non-type metadata, but it may surprise you to learn that there is also a way to do calculations with types. To get a feeling for what that meansand why it matterswe're going to look at one of the simplest algorithms from the C++ standard library: iter_swap. It is iter_swap's humble duty to take two iterators and exchange the values of the objects they refer to. It looks something like this:
template <class ForwardIterator1, class ForwardIterator2>
void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2)
{
T tmp = *i1;
*i1 = *i2;
*i2 = tmp;
}
If at this point you're wondering where T came from, you've got a sharp eye. It hasn't been defined, and iter_swap can't compile if we write it that way. Informally, of course, T is the type you get when the iterator is dereferenced, what's known in the C++ standard (section 24.1) as the iterator's value type. Okay, but how do we name that type? 2.1.1. Using a Direct ApproachIn case you already know the answer chosen by the authors of the standard library, we'll ask you to forget it for the time being; we have a couple of deeper points to make. Instead, imagine we're implementing the standard library ourselves and choosing its method of handling iterators. We're going to end up writing a lot of algorithms, and many of them will need to make an association between an iterator type and its value type. We could require all iterator implementations to supply a nested type called value_type, which we'd access directly: template <class ForwardIterator1, class ForwardIterator2> void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2) { typename // (see Language Note) ForwardIterator1::value_type tmp = *i1; *i1 = *i2; *i2 = tmp; }
That's a perfectly good strategy for making type associations, but it's not very general. In particular, iterators in C++ are modeled on the design of pointers, with the intention that plain pointers should themselves be valid iterators. Unfortunately, pointers can't have nested types; that privilege is reserved for classes:
void f(int* p1, int* p2)
{
iter_swap(p1,p2); // error: int* has no member 'value_type'
}
2.1.2. Taking the Long Way Around
Lampson's idea is so universal in programming that Andrew Koenig[2] is fond of calling it "the Fundamental Theorem of Software Engineering" (FTSE). We may not be able to add a nested ::value_type to all iterators, but we can add it to a template that takes the iterator type as a parameter. In the standard library this template, called iterator_traits, has a simple signature:
template <class Iterator> struct iterator_traits; Here's how we put it to work in iter_swap:
template <class ForwardIterator1, class ForwardIterator2>
void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2)
{
typename
iterator_traits<ForwardIterator1>::value_type tmp = *i1;
*i1 = *i2;
*i2 = tmp;
}
iterator_traits is so named because it describes properties (or traits) of its argument. In this case, the traits being described are the iterator's five associated types: value_type, reference, pointer, difference_type, and iterator_category. The most important feature of traits templates is that they give us a way to associate information with a type non-intrusively. In other words, if your ornery coworker Hector gives you some iterator-like type called hands_off that refers to an int, you can assign it a value_type without disturbing the harmony of your workgroup. All you have to do is add an explicit specialization of iterator_traits, and iter_swap will see the type int when it asks about the value_type of Hector's iterator:[3]
namespace std { template <> struct iterator_traits<Hector::hands_off> { typedef int value_type; four more typedefs... }; } This non-intrusive aspect of traits is precisely what makes iterator_traits work for pointers: the standard library contains the following partial specialization of iterator_traits, which describes the value_type of all pointers: template <class T> struct iterator_traits<T*> { typedef T value_type; ...four more typedefs }; Thanks to the indirection through iterator_traits, generic functions can now access an iterator's associated types uniformly, whether or not it happens to be a pointer. 2.1.3. Finding a ShortcutWhile specialization is a perfectly general mechanism, it's not nearly as convenient as adding nested types to classes. Specialization comes with a lot of baggage: You may have to close the namespaces you're working in and open the namespace of the traits template, and then you'll have to write the text of the traits specialization itself. That's not a very efficient use of keystrokes: its nested typedef is the only information that really counts for anything. Thoughtfully, the standard library provides a shortcut that allows the author of an iterator to control the types nested in its iterator_traits just by writing member types in the iterator. The primary iterator_traits template[4] reaches into the iterator to grab its member types:
template <class Iterator> struct iterator_traits { typedef typename Iterator::value_type value_type; ...four more typedefs }; Here you can see the "extra level of indirection" at work: Instead of going directly to Iterator:: value_type, iter_swap gets there by asking iterator_traits for the iterator's value_type. Unless some specialization overrides the primary iterator_traits template, iter_swap sees the same value_type as it would have if it had directly accessed a nested type in the iterator. ![]() |