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.
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); }
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
).
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.