I l@ve RuBoard |
![]() ![]() |
2.10 Type TraitsTraits are a generic programming technique that allows compile-time decisions to be made based on types, much as you would make runtime decisions based on values (Alexandrescu 2000a). By adding the proverbial "extra level of indirection" that solves many software engineering problems, traits let you take type-related decisions outside the immediate context in which they are made. This makes the resulting code cleaner, more readable, and easier to maintain. Usually you will write your own trait templates and classes as your generic code needs them. Certain traits, however, are applicable to any type. They can help generic programmers to tailor template code better to the capabilities of a type. Suppose, for instance, that you implement a copying algorithm: template <typename InIt, typename OutIt> OutIt Copy(InIt first, InIt last, OutIt result) { for (; first != last; ++first, ++result) *result = *first; } In theory, you shouldn't have to implement such an algorithm, because it duplicates the functionality of std::copy. But you might need to specialize your copying routine for specific types. Let's say you develop code for a multiprocessor machine that has a very fast BitBlast primitive function, and you would like to take advantage of that primitive whenever possible. // Prototype of BitBlast in "SIMD_Primitives.h" void BitBlast(const void* src, void* dest, size_t bytes); BitBlast, of course, works only for copying primitive types and plain old data structures. You cannot use BitBlast with types having a nontrivial copy constructor. You would like, then, to implement Copy so as to take advantage of BitBlast whenever possible, and fall back on a more general, conservative algorithm for elaborate types. This way, Copy operations on ranges of primitive types will "automagically" run faster. What you need here are two tests:
If you can find answers to these questions at compile time and if the answer to both questions is yes, you can use BitBlast. Otherwise, you must rely on the generic for loop. Type traits help in solving such problems. The type traits in this chapter owe a lot to the type traits implementation found in the Boost C++ library (Boost). 2.10.1 Implementing Pointer TraitsLoki defines a class template TypeTraits that collects a host of generic type traits. TypeTraits uses template specialization internally and exposes the results. The implementation of most type traits relies on total or partial template specialization (Section 2.2). For example, the following code determines whether a type T is a pointer:
template <typename T>
class TypeTraits
{
private:
template <class U> struct PointerTraits
{
enum { result = false };
typedef NullType PointeeType;
};
template <class U> struct PointerTraits<U*>
{
enum { result = true };
typedef U PointeeType;
};
public:
enum { isPointer = PointerTraits<T>::result };
typedef PointerTraits<T>::PointeeType PointeeType;
...
};
The first definition introduces the PointerTraits class template, which says, "T is not a pointer, and a pointee type doesn't apply." Recall from Section 2.9 that NullType is a placeholder type for "doesn't apply" cases. The second definition (the line in bold) introduces a partial specialization of PointerTraits, a specialization that matches any pointer type. For pointers to anything, the specialization in bold qualifies as a better match than the generic template for any pointer type. Consequently, the specialization enters into action for a pointer, so result evaluates to true. In addition, PointeeType is defined appropriately. You can now gain some insight into the std::vector::iterator implementation—is it a plain pointer or an elaborate type? int main() { const bool iterIsPtr = TypeTraits<vector<int>::iterator>::isPointer; cout << "vector<int>::iterator is " << iterIsPtr ? "fast" : "smart" << '\n'; } Similarly, TypeTraits implements an isReference constant and a ReferencedType type definition. For a reference type T, ReferencedType is the type to which T refers; if T is a straight type, ReferencedType is T itself. Detection of pointers to members (consult Chapter 5 for a description of pointers to members) is a bit different. The specialization needed is as follows: template <typename T> class TypeTraits { private: template <class U> struct PToMTraits { enum { result = false }; }; template <class U, class V> struct PToMTraits<U V::*> { enum { result = true }; }; public: enum { isMemberPointer = PToMTraits<T>::result }; ... }; 2.10.2 Detection of Fundamental TypesTypeTraits<T> implements an isStdFundamental compile-time constant that says whether or not T is a standard fundamental type. Standard fundamental types consist of the type void and all numeric types (which in turn are floating-point and integral types). TypeTraits defines constants that reveal the categories to which a given type belongs. At the price of anticipating a bit, it should be said that the magic of typelists (Chapter 3) makes it easy to detect whether a type belongs to a known set of types. For now, all you should know is that the expression
TL::IndexOf<T, TYPELIST_nn(comma-separated list of types)>::value
(where nn is the number of types in the list of types) returns the zero-based position of T in the list, or –1 if T does not figure in the list. For example, the expression TL::IndexOf<T, TYPELIST_4(signed char, short int, int, long int)>::value is greater than or equal to zero if and only if T is a signed integral type. Following is the definition of the part of TypeTraits dedicated to primitive types. template <typename T> class TypeTraits { ... as above ... public: typedef TYPELIST_4( unsigned char, unsigned short int, unsigned int, unsigned long int) UnsignedInts; typedef TYPELIST_4(signed char, short int, int, long int) SignedInts; typedef TYPELIST_3(bool, char, wchar_t) OtherInts; typedef TYPELIST_3(float, double, long double) Floats; enum { isStdUnsignedInt = TL::IndexOf<T, UnsignedInts>::value >= 0 }; enum { isStdSignedInt = TL::IndexOf<T, SignedInts>::value >= 0 }; enum { isStdIntegral = isStdUnsignedInt || isStdSignedInt || TL::IndexOf <T, OtherInts>::value >= 0 }; enum { isStdFloat = TL::IndexOf<T, Floats>::value >= 0 }; enum { isStdArith = isStdIntegral || isStdFloat }; enum { isStdFundamental = isStdArith || isStdFloat || Conversion<T, void>::sameType }; ... }; Using typelists and TL::IndexOf gives you the ability to infer information quickly about types, without having to specialize a template many times. If you cannot resist the temptation to delve into the details of typelists and TL::IndexOf, take a peek at Chapter 3—but don't forget to return here. The actual implementation of detection of fundamental types is more sophisticated, allowing for vendor-specific extension types (such as int64 or long long). 2.10.3 Optimized Parameter TypesIn template code, you sometimes need to answer the following question: Given an arbitrary type T, what is the most efficient way of passing and accepting objects of type T as arguments to functions? In general, the most efficient way is to pass elaborate types by reference and scalar types by value. (Scalar types consist of the arithmetic types described earlier as well as enums, pointers, and pointers to members.) For elaborate types you avoid the overhead of an extra temporary (constructor-plus-destructor calls), and for scalar types you avoid the overhead of the indirection resulting from the reference. A detail that must be carefully handled is that C++ does not allow references to references. Thus, if T is already a reference, you should not add one more reference to it. A bit of analysis on the optimal parameter type for a function call engenders the following algorithm. Let's call the parameter type that we look for ParameterType.
One important achievement of this algorithm is that it avoids the reference-to-reference error, which might appear if you combined bind2nd with mem_fun standard library functions. It is easy to implement TypeTraits::ParameterType using the techniques we already have in hand and the traits defined earlier—ReferencedType and isPrimitive. template <typename T> class TypeTraits { ... as above ... public: typedef Select<isStdArith || isPointer || isMemberPointer, T, ReferencedType&>::Result ParameterType; }; Unfortunately, this scheme fails to pass enumerated types (enums) by value because there is no known way of determining whether or not a type is an enum. The Functor class template defined in Chapter 5 uses TypeTraits::ParameterType. 2.10.4 Stripping QualifiersGiven a type T, you can easily get to its constant sibling by simply typing const T. However, doing the opposite (stripping the const off a type) is slightly harder. Similarly, you sometimes might want to get rid of the volatile qualifier of a type. Consider, for instance, building a smart pointer class SmartPtr (Chapter 7 discusses smart pointers in detail). Although you would like to allow users to create smart pointers to const objects—as in SmartPtr<const Widget>—you still need to modify the pointer to Widget you hold internally. In this case, inside SmartPtr you need to obtain Widget from const Widget. Implementing a "const stripper" is easy, again by using partial template specialization: template <typename T> class TypeTraits { ... as above ... private: template <class U> struct UnConst { typedef U Result; }; template <class U> struct UnConst<const U> { typedef U Result; }; public: typedef UnConst<T>::Result NonConstType; }; 2.10.5 Using TypeTraitsTypeTraits can help you do a lot of interesting things. For one thing, you can now implement the Copy routine to use BitBlast (the problem mentioned in Section 2.10) by simply assembling techniques presented in this chapter. You can use TypeTraits to figure out type information about the two iterators and the Int2Type template for dispatching the call either to BitBlast or to a classic copy routine. enum CopyAlgoSelector { Conservative, Fast }; // Conservative routine-works for any type template <typename InIt, typename OutIt> OutIt CopyImpl(InIt first, InIt last, OutIt result, Int2Type<Conservative>) { for (; first != last; ++first, ++result) *result = *first; return result; } // Fast routine-works only for pointers to raw data template <typename InIt, typename OutIt> OutIt CopyImpl(InIt first, InIt last, OutIt result, Int2Type<Fast>) { const size_t n = last-first; BitBlast(first, result, n * sizeof(*first)); return result + n; } template <typename InIt, typename OutIt> OutIt Copy(InIt first, InIt last, OutIt result) { typedef TypeTraits<InIt>::PointeeType SrcPointee; typedef TypeTraits<OutIt>::PointeeType DestPointee; enum { copyAlgo = TypeTraits<InIt>::isPointer && TypeTraits<OutIt>::isPointer && TypeTraits<SrcPointee>::isStdFundamental && TypeTraits<DestPointee>::isStdFundamental && sizeof(SrcPointee) == sizeof(DestPointee) ? Fast : Conservative }; return CopyImpl(first, last, result, Int2Type<copyAlgo>); } Although Copy itself doesn't do much, the interesting part is right in there. The enum value copyAlgo selects one implementation or the other. The logic is as follows: Use BitBlast if the two iterators are pointers, if both pointed-to types are fundamental, and if the pointed-to types are of the same size. The last condition is an interesting twist. If you do this: int* p1 = ...; int* p2 = ...; unsigned int* p3 = ...; Copy(p1, p2, p3); then Copy calls the fast routine, as it should, although the source and destination types are different. The drawback of Copy is that it doesn't accelerate everything that could be accelerated. For example, you might have a plain C-like struct containing nothing but primitive data—a so-called plain old data, or POD, structure. The standard allows bitwise copying of POD structures, but Copy cannot detect "PODness," so it will call the slow routine. Here you have to rely, again, on classic traits in addition to TypeTraits. For instance: template <typename T> struct SupportsBitwiseCopy { enum { result = TypeTraits<T>::isStdFundamental }; }; template <typename InIt, typename OutIt> OutIt Copy(InIt first, InIt last, OutIt result, Int2Type<true>) { typedef TypeTraits<InIt>::PointeeType SrcPointee; typedef TypeTraits<OutIt>::PointeeType DestPointee; enum { useBitBlast = TypeTraits<InIt>::isPointer && TypeTraits<OutIt>::isPointer && SupportsBitwiseCopy<SrcPointee>::result && SupportsBitwiseCopy<DestPointee>::result && sizeof(SrcPointee) == sizeof(DestPointee) }; return CopyImpl(first, last, Int2Type<useBitBlast>); } Now, to unleash BitBlast for your POD types of interest, you need only specialize SupportsBitwiseCopy and put a true in there: template<> struct SupportsBitwiseCopy<MyType> { enum { result = true }; }; 2.10.6 Wrapping UpTable 2.1 defines the complete set of traits implemented by Loki. ![]() |
I l@ve RuBoard |
![]() ![]() |