Type traits

... by popular demand. Here we go! We take a look at how to obtain a type-traits implementation for C++, what they are good for and go through some easy examples to understand what they do.

Where to get them

Before we start, you'll need a type-traits implementation. Boost has a very nice one, which is fully modularized. You can include it by using #include which will put it into the std::tr1 namespace that I will use in the examples. Optionally, the TR1 additions to C++ contain it (for example, the Visual C++ Feature Pack has TR1 support).

What are they

Well, type-traits allow you to make compile-time decisions based on the properties of a type. Sounds kinda complicated? Well, think about it like this: You have a template class, and the user passes in a type you never heard about. With type-traits, you can get information about the type. The result of a type-trait is passed by its member value which is either true or false. Moreover, there is a type which is a real type (integral_constant, but I'll omit it here and write a small bool-wrapper-type). We'll be using this helper structure which turns a boolean into a type:

struct True {};
struct False {};
template <bool b> struct BoolToType;
template <> struct BoolToType<true>
{
    typedef True type;
};

template <> struct BoolToType<false>
{
    typedef False type;
};

Obviously, BoolToType<true>::type is True. This comes in handy later.

How to make use of them

Let's assume we are implementing a copy routine: fast_copy (const T* source, T* destination, size_type count). Now, if T has a trivial assign implementation (trivial meaning an assign implementation which is just a memcpy) we can just use memcpy, otherwise we have to go into a loop and copy using T::operator=. Now, how can we make the decision at compile-time whether it is a type with a trivial assign or not? You guessed it, with type-traits, specifially has_trivial_assign.

#include <type_traits>
#include <cstring>
#include <string>

#include "boolToType.h" // BoolToType helper
// Fast path
template <typename T, typename size_type>
void fast_copy_impl (const T* source, T* destination, const size_type count, const True&)
{
    std::memcpy (destination, source, count * sizeof (T));
}

// Slow path
template <typename T, typename size_type> void fast_copy_impl (const T* source, T* destination, const size_type count, const False&)
{
    for (size_type i = size_type (); i < count; ++i) {
        *destination++ = *source++;
    }
}

template <typename T, typename size_type> void fast_copy (const T* source, T* destination, const size_type count)
{
    BoolToType<std::tr1::has_trivial_assign<T>::value>::type canUseFastPath;
    fast_copy_impl (source, destination, count, canUseFastPath);
}

int main ()
{
    std::string arrComplex [10];
    int arrSimple [20]; // Will call the loop version fast_copy (arrComplex, arrComplex + 5, 5);
    // VC++9 can roll this out to direct copy via registers (!)
    fast_copy (arrSimple, arrSimple + 10, 10);
}

We turn the boolean result of the type-traits into a type by using the helper (as said above, you could directly overload based on integral_constant), and use it to dispatch between two template functions. Voilà , based on the type, we can choose to use memcpy if possible. And the good thing is, this will work for any type. This is something you have no chance to implement on your own as this type-trait relies on (non-portable) compiler support! Another example, how to skip calling T::~T if the destructor does nothing. This can help if you have a large array, and you want to get rid of it quickly - if the destructor is trivial, you can just throw it away, otherwise, you need to clear it. This can be interesting if you have some "scratch-memory" where you allocate blocks from. Here we go:

template <typename T, typename size_type>
void array_delete_impl (T* array, const size_type count, const False&)
{
    for (size_type i = size_type (); i < count; ++i) {
        array[i].~T();
    }
}

template <typename T, typename size_type> void array_delete_impl (T* array, const size_type count, const True&) {}

template <typename T, typename size_type> void array_delete (T*
array, const size_type count)
{
    array_delete_impl (array, count, BoolToType<std::tr1::has_trivial_destructor<T>::value>::type ());
}

Conclusion

Type-traits are a very powerful tool which can both improve the efficiency of template code and improve the maintainability by not relying on custom traits specialisations. The examples here are just very basic - just take a look at how many type traits are available. In addition, there are special type-traits like add_reference which transform types: They add a qualifier if possible (otherwise, you could wind up having a const T& & which is not allowed in current C++ -- will be allowed in C++0x).

Comments

Comments powered by Disqus