Team LiB   Previous Section   Next Section

8.4 Traits and Policies

Traits are used throughout the C++ library. A trait is a class or class template that characterizes a type, possibly a template parameter. At first glance, it seems that traits obscure information, hiding types and other declarations in a morass of templates. This is true, but traits are also powerful tools used in writing templates. Traits are often used to obtain information from built-in types in the same manner as user-defined types.

A policy is a class or class template that defines an interface as a service to other classes. Traits define type interfaces, and policies define function interfaces, so they are closely related. Sometimes, a single class template implements traits and policies.

The typical application programmer might never use traits and policies directly. Indirectly, however, they are used in the string class, I/O streams, the standard containers, and iterators—just about everywhere in the standard library.

8.4.1 Character Traits

One of the most commonly used trait and policy templates is char_traits<> in the <string> header. The standard declares two specializations: char_traits<char> and char_traits<wchar_t>.

The rest of the C++ library uses character traits to obtain types and functions for working with characters. For example, the basic_istream class template takes a character type, charT, and a character traits type as template parameters. The default value for the traits parameter is char_traits<charT>, which is a set of character traits defined in <string>. The basic_istream template declares the get( ) function, which reads a character and returns its integer equivalent. The return type is obtained from the character traits template, specifically int_type.

As a policy template, char_traits<> provides member functions that compare characters and character arrays, copy character arrays, and so on. For example, compare compares two character arrays for equality. The char_traits<char> specialization might implement compare by calling memcmp.

At a basic level, the typical C++ programmer does not need to be concerned with the implementation of traits. Instead, you can use the istream and string classes, and everything just works. If you are curious, you can trace the declaration of, for example, istream::int_type:

istream::int_type basic_istream<char>::int_type traits::int_type char_traits<char>::int_type int

As you can see, traits can be difficult to follow when you need to know the exact type of one of the types declared in a standard container.

Once you get used to them, however, you can see how valuable traits can be. Consider what happens when you change from istream::int_type to wistream::int_type:

wistream::int_type basic_istream<wchar_t>::int_type traits::int_type char_traits<wchar_t>::int_type wint_t

Note that the declarations of basic_istream and the other templates do not differ when the template parameter changes from char to wchar_t. Instead, you end up with a different template specialization for char_traits<>, which directs you to a different integer type.

You can implement your own character traits and policy template. For example, suppose you want to use strings that compare themselves without regard to case differences. Comparison is a policy issue, typically implemented by the char_traits<> template. You can define your own template that has the same trait and policy implementation, but one that implements compare to ignore case differences. Using your template, you can specialize basic_string<> to create a case-insensitive string class and then store those strings in sets and maps. The keys will be compared using your policy function that ignores case differences, as shown in Example 8-1.

Example 8-1. Case-insensitive character policy
template<typename T> struct ci_char_traits {};
template<> struct ci_char_traits<char> {
  typedef char char_type;
  typedef int int_type;
  typedef std::streamoff off_type;
  typedef std::streampos pos_type;
  typedef std::mbstate_t state_type;

  static void assign(char_type& dst, const char_type src) {
    dst = src;
  }
  static char_type* assign(char* dst, std::size_t n, char c) {
    return static_cast<char_type*>(std::memset(dst, n, c));
  }
  static bool eq(const char_type& c1, const char_type& c2) {
    return lower(c1) == lower(c2);
  }
  static bool lt(const char_type& c1, const char_type& c2) {
    return lower(c1) < lower(c2);
  }
  static int compare(const char_type* s1,
  const char_type* s2, std::size_t n) {
    for (size_t i = 0; i < n; ++i)
    {
      char_type lc1 = lower(s1[i]);
      char_type lc2 = lower(s2[i]);
      if (lc1 < lc2)
        return -1;
      else if (lc1 > lc2)
        return 1;
    }
    return 0;
  }
  ...
private:
  static int_type lower(char_type c) {
    return std::tolower(to_int_type(c));
  }
};

typedef std::basic_string<char, ci_char_traits<char> >
  ci_string;

void print(const std::pair<const ci_string, std::size_t>& item)
{
  std::cout << item.first << '\t' << item.second << '\n';
}

int main(  )
{
  std::map<ci_string, std::size_t> count;
  ci_string word;
  while (std::cin >> word)
    ++count[word];
  std::for_each(count.begin(  ), count.end(  ), print);
}

8.4.2 Iterator Traits

Traits are also useful for iterators (Chapter 10). An algorithm often needs to know the iterator category to provide specializations that optimize performance for random access iterators, for example. Traits provide a standard way to convey this information to the algorithm — namely, by using the iterator_category typedef. They also permit algorithms to use plain pointers as iterators.

For example, the distance function returns the distance between two iterators. For random access iterators, the distance can be computed by subtraction. For other iterators, the distance must be computed by incrementing an iterator and counting the number of increments needed. Example 8-2 shows a simple implementation of distance that uses the iterator traits to choose the optimized random access implementation or the slower implementation for all other input iterators.

Example 8-2. Implementing the distance function template
// Helper function, overloaded for random access iterators
template<typename InputIter>
typename std::iterator_traits<InputIter>::difference_type
compute_dist(InputIter first, InputIter last,
             std::random_access_iterator_tag)
{
  return last - first;
}

// Helper function, overloaded for all other input iterators
template<typename InputIter>
typename std::iterator_traits<InputIter>::difference_type
compute_dist(InputIter first, InputIter last,
             std::input_iterator_tag)
{
  typename std::iterator_traits<InputIter>::difference_type
    count = 0;
  while (first != last) {
    ++first;
    ++count;
  }
  return count;
}

// Main distance function, which calls the helper function, using the iterator
// tag to differentiate the overloaded functions.
template<typename InputIter>
typename std::iterator_traits<InputIter>::difference_type
distance(InputIter first, InputIter last)
{
  return compute_dist(first, last,
    std::iterator_traits<InputIter>::iterator_category(  ));
}

Being able to optimize algorithms for certain kinds of iterators is one benefit of using traits, but the real power comes from the iterator_traits<T*> specialization. This class permits the use of any pointer type as an iterator. (See <iterator> in Chapter 13 for details.) Consider how the distance function is called in the following example:

int data[] = { 10, 42, 69, 13, 100, -1 };
distance(&data[1], &data[4]);

The compiler infers the InputIter template parameter as type int*. The iterator_traits<T*> template is expanded to obtain the iterator_category type (random_access_iterator_tag) and difference_type (ptrdiff_t).

8.4.3 Custom Traits

Traits can be useful whenever you are using templates. You never know what the template parameters might be. Sometimes, you want to specialize your own code according to a template parameter.

For example, all the standard sequence containers have a constructor that takes two iterators as arguments:

template<typename InputIterator>
list(InputIterator first, InputIterator last);

But take a closer look at the declaration. The author's intent is clear: that the template parameter must be an input iterator, but nothing in the declaration enforces this restriction. The compiler allows any type to be used (at least any type that can be copied).

If the InputIterator type actually is an input iterator, the list is constructed by copying all the elements in the range [first, last). But if the InputIterator type is an integral type, the first argument is interpreted as a count, and the last argument is interpreted as an integer value, which is converted to the value type of the container; the container is then initialized with first copies of the last value, which is ordinarily the work of a different constructor. See Chapter 10 for more information about these constructors.

If you need to implement your own container template, you must find a way to implement this kind of constructor. The simplest way is to define a traits template that can tell you whether a type is an integral type. Example 8-3 shows one possible implementation and how it can be used by a container.

Example 8-3. Differentiating between integral types using traits
// Type trait to test whether a type is an integer.
struct is_integer_tag {};
struct is_not_integer_tag {};

// The default is that a type is not an integral type.
template<typename T>
struct is_integer {
  typedef is_not_integer_tag tag;
};

// Override the default explicitly for all integral types.
template<>
struct is_integer<int> {
  typedef is_integer_tag tag;
};
template<>
struct is_integer<short> {
  typedef is_integer_tag tag;
};
template<>
struct is_integer<unsigned> {
  typedef is_integer_tag tag;
};
// And so on for char, signed char, short, etc.

// Constructor uses the is_integer trait to distinguish integral from nonintegral
// types and dispatches to the correct overloaded construct function.
template<typename T, typename A>
template<typename InputIter>
list<T,A>::list(InputIter first, InputIter last)
{
  construct(first, last, is_integer<InputIter>::tag(  ));
}

// The construct member functions finish the initialization of the list. The
// integral version casts the arguments to the size and value types.
template<typename T, typename A>
template<typename InputIter>
void list<T,A>::construct(InputIter first, InputIter last,
                          is_integer_tag)
{
  insert(begin(  ), static_cast<size_type>(first),
                  static_cast<T>(last));
}

// The non-integral version copies elements from the iterator range.
template<typename T, typename A>
template<typename InputIter>
void list<T,A>::construct(InputIter first, InputIter last,
                          is_not_integer_tag)
{
  insert(begin(  ), first, last);
}

Traits can be used to characterize any type and specialize templates for a wide variety of situations. See the Boost project (described in Appendix B) for other definitions and uses of traits.

    Team LiB   Previous Section   Next Section