Effective C++: Item 47 -- Use traits classes for information about types(check type in compile time)

Mini-review on STL Iterator Category

5 categories of iterators:

  1. Input iterators can move only forward, can move only one step at a time, can only read what they point to, and can read what they’re pointing to only once. They’re modeled on the read pointer into an input file; the C++ library’s istream_iterators are representative of this category.
  2. Output iterators are analogous, but for output: they move only forward, move only one step at a time, can only write what they point to, and can write it only once. They’re modeled on the write pointer into an output file; ostream_iterators epitomize this category.
  3. forward iterators: Such iterators can do everything input and output iterators can do, plus they can read or write what they point to more than once. This makes them viable for multi-pass algorithms.
  4. Bidirectional iterators add to forward iterators the ability to move backward as well as forward. Iterators for the STL’s list are in this category, as are iterators for set, multiset, map, and multimap
  5. The most powerful iterator category is that of random access iterators. These kinds of iterators add to bidirectional iterators the ability to perform “iterator arithmetic,” i.e., to jump forward or backward an arbi- trary distance in constant time. Iterators for vector, deque, and string are random access iterators.

C++ Tag Struct to identify them

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};

Example of Making advance

template<typename IterT, typename DistT> 	// move iter d units
void advance(IterT& iter, DistT d);			// forward; if d < 0,
											// move iter backward

Conceptually, advance just does iter += d, but advance can’t be implemented that way, because only random access iterators support the += operation. Less powerful iterator types have to implement advance by iteratively applying ++ or – d times.

What we really want to do is implement advance essentially like this:

template<typename IterT, typename DistT> 
void advance(IterT& iter, DistT d)
{
	if (iter is a random access iterator) { 
		iter += d;
	} else {
		if (d >= 0) { while (d--) ++iter; } 
		else { while (d++) --iter; }
	}
}

This requires being able to determine whether iter is a random access iterator, which in turn requires knowing whether its type, IterT, is a random access iterator type. That’s what traits let you do: they allow you to get information about a type during compilation.

Traits

Traits aren’t a keyword or a predefined construct in C++; they’re a technique and a convention followed by C++ programmers. One of the demands made on the technique is that it has to work as well for built-in types as it does for user-defined types. The fact that traits must work with built-in types means that things like nesting information inside types won’t do, because there’s no way to nest information inside pointers.
The standard technique is to put it into a template and one or more specializations of that template.
For iterators, the template in the standard library is named iterator_traits:

template<typename IterT> // template for information about 
struct iterator_traits; // iterator types

By convention, traits are always implemented as structs. Another convention is that the structs used to implement traits are known as — traits classes.
The way iterator_traits works is that for each type IterT, a typedef named iterator_category is declared in the struct iterator_traits<IterT>. This typedef identifies the iterator category of IterT.

iterator_traits implements this in two parts

  1. it imposes the requirement that any user-defined iterator type must contain a nested typedef named iterator_category that identifies the appropriate tag struct. deque’s iterators are random access, for example, so a class for deque iterators would look something like this:
    template < ... > // template params elided 
    class deque {
    public:
    	class iterator { 
    	public:
    		typedef random_access_iterator_tag iterator_category;
    		... 
    	};
    	... 
    };
    
    iterator_traits just parrots back the iterator class’s nested typedef:
    // the iterator_category for type IterT is whatever IterT says it is; 
    // see Item 42 for info on the use of “typedef typename” 
    // -- Nested Dependent Name
    template<typename IterT>
    struct iterator_traits {
    	typedef typename IterT::iterator_category iterator_category;
    	... 
    };
    
    This works well for user-defined types, but it doesn’t work at all for iterators that are pointers, because there’s no such thing as a pointer with a nested typedef.
  2. To support such iterators, iterator_traits offers a partial template specialization for pointer types. Pointers act as random access iterators, so that’s the category iterator_traits specifies for them:
    template<typename T> // partial template specialization 
    struct iterator_traits<T*> // for built-in pointer types
    {
    	typedef random_access_iterator_tag iterator_category;
    	... 
    };
    

At this point, you know how to design and implement a traits class:

  • Identify some information about types you’d like to make available (e.g., for iterators, their iterator category).
  • Choose a name to identify that information (e.g., iterator_category).
  • Provide a template and set of specializations (e.g., iterator_traits)
    that contain the information for the types you want to support.

We can now use Traits class in the advance example

template<typename IterT, typename DistT> 
void advance(IterT& iter, DistT d)
{
	if (typeid(typename std::iterator_traits<IterT>::iterator_category) == 
			typeid(std::random_access_iterator_tag))
	... 
}

Problem: If statement is evaluated at runtime

Although this looks promising, it’s not what we want. IterT’s type is known during compilation, so iterator_traits<IterT>::iterator_category can also be determined during compilation. Yet the if statement is evaluated at runtime (unless your optimizer is crafty enough to get rid of it). Why do something at runtime that we can do during compilation? It wastes time (literally), and it bloats our executable.
What we really want is a conditional construct (i.e., an if…else state- ment) for types that is evaluated during compilation. As it happens, C++ already has a way to get that behavior. It’s called overloading.

Solution: Overloading

To get advance to behave the way we want, all we have to do is create multiple versions of an overloaded function containing the “guts” of advance, declaring each to take a different type of iterator_category object. I use the name doAdvance for these functions:

template<typename IterT, typename DistT> 			// use this impl for
void doAdvance(IterT& iter, DistT d, 				// random access
				std::random_access_iterator_tag)	// iterators
{
	iter += d;
}

template<typename IterT, typename DistT> 			// use this impl for
void doAdvance(IterT& iter, DistT d,				// bidirectional
				std::bidirectional_iterator_tag)	// iterators
{
	if (d >= 0) { while (d--) ++iter; } 
	else { while (d++) --iter; }
}

template<typename IterT, typename DistT> 			// use this impl for
void doAdvance(IterT& iter, DistT d,				// input iterators
				std::input_iterator_tag)			
	if (d < 0){
		throw std::out_of_range("Negative distance");// see below
	}
	while (d--) ++iter; 
}

Because forward_iterator_tag inherits from input_iterator_tag, the version of doAdvance for input_iterator_tag will also handle forward iteraors.

Use Overloads in advance

template<typename IterT, typename DistT> 
void advance(IterT& iter, DistT d)
{
	doAdvance( 											// call the version
		iter, d,										// of doAdvance
		typename 										// that is
			std::iterator_traits<IterT>::iterator_category()
														// appropriate for										
	); 													// iter’s iterator
}														// category

We can now summarize how to use a traits class:

  • Create a set of overloaded “worker” functions or function tem- plates (e.g., doAdvance) that differ in a traits parameter. Implement each function in accord with the traits information passed.
  • Create a “master” function or function template (e.g., advance) that calls the workers, passing information provided by a traits class.

Conclusion

Traits are widely used in the standard library. There’s iterator_traits, of course, which, in addition to iterator_category, offers four other pieces of information about iterators (the most useful of which is value_type — Item 42 shows an example of its use). TR1 (see Item 54) introduces a slew of new traits classes that give infor- mation about types, including is_fundamental (whether T is a built-in type), is_array (whether T is an array type), and is_base_of<T1, T2> (whether T1 is the same as or is a base class of T2). All told, TR1 adds over 50 traits classes to standard C++.

Things to Remember

  • Traits classes make information about types available during compilation. They’re implemented using templates and template specializations.
  • In conjunction with overloading, traits classes make it possible to perform compile-time if…else tests on types.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值