Kuro5hin.org: technology and culture, from the trenches
create account | help/FAQ | contact | links | search | IRC | site news
[ Everything | Diaries | Technology | Science | Culture | Politics | Media | News | Internet | Op-Ed | Fiction | Meta | MLP ]
We need your support: buy an ad | premium membership

[P]
Programming Tips: 2) An introduction to C++ Traits

By codemonkey_uk in Columns
Thu Mar 01, 2001 at 10:00:35 AM EST
Tags: Software (all tags)
Software

In the past, reusable code meant a sacrifice of efficiency for convenience. The C standard library qsort performs comparisons via a function pointer. OOP dosen't help, inheritance and virtual functions are still an overhead.

C++ introduces generic programming, with templates, eliminating the need for runtime binding, but at first glance this still looks like a compromise, after all, the same algorithm will not work optimally with every data structure. Sorting a linked list is different to sorting an array. Sorted data can be searched much faster than unsorted data.

The C++ traits technique provides an answer.


Think of a trait as a small object whose main purpose is to carry information used by another object or algorithm to determine "policy" or "implementation details".
- Bjarne Stroustrup
Both C and C++ programmers should be familiar with limits.h, and float.h, which are used to determine the various properties of the integer and floating point types.

Most C++ programmers are familiar with std::numeric_limits, which at first glance simply provides the same service, implemented differently. By taking a closer look at numeric_limits we uncover the first advantage of traits, a consistent interface.

Using float.h, and limits.h, you have to remember the type prefix and the trait, for example, DBL_MAX contains the "maximum value" trait for the "double" data type. By using a traits class such as numeric_limits the type becomes part of the name, so that the maximum value for a double becomes numeric_limits< double >::max(), more to the point, you don't need to know which type you need to use. For example, take this simple template function [2], which returns the largest value in an array:

template< class T >
T findMax(const T* data, int numItems)
{
    // Obtain the minimum value for type T
    T largest = std::numeric_limits<T>::min();

    for (int i=0; i < numItems; ++i)
        if (data[i] > largest)
            largest = data[i];

    return largest;
}
Note the use of numeric_limits. As you can see, where as with the C style limits.h idiom, where you must know the type, with the C++ traits idiom, only the compiler needs to know the type. Not only that, but numeric_limits, as with most traits, can be extended to include your own custom types (such as a fixed point, or arbitrary precision arithmetic classes) simply by creating a specialisation of the template.

But I'd like to move away from numeric_limits, its just an example of traits in action, and I'd like to take you through creating traits classes of your own.

First lets look at one of the simplest traits classes you can get (from boost.org) and that's the is_void [3] trait.

First, a generic template is defined that implements the default behaviour. In this case, all but one type is void, so is_void::value should be false, so we start with:

template< typename T >
struct is_void{
    static const bool value = false;
};
Add to that a specialisation for void:
template<>
struct is_void< void >{
    static const bool value = true;
};
And we have a complete traits type that can be used to detect if any given type, passed in as a template parameter, is void. Not the most useful piece of code on its own, but definitely a useful demonstration of the technique.

Now, while fully specialised templates are useful and in my experiance, the most common sort of trait class specialisation, I think that it is worth quickly looking at partial specialisation, in this case, boost::is_pointer [3]. Again, a default template class is defined:

template< typename T >
struct is_pointer{
    static const bool value = false;
};
And a partial specialisation for all pointer types is added:
template< typename T >
struct is_pointer< T* >{
    static const bool value = true;
};
So, having got this far, how can this technique be used to solve the lowest common denominator problem? How can it be used to select an appropriate algorythm at compile time? This is best demonstrated with an example.

First a default traits class is created, for this example we'll call it supports_optimised_implementation, and, other than the name, it will be the same is the is_void example. Next the default algorithm is implemented, inside a templated algorithm_selector, in this example the algorithm_selector template is parameterised with a bool, but in situations where a number of algorithms could be appropriate it could just as easily be parameterised with an int, or and enum. In this case "true" will mean "use optimised algorithm in object".

template< bool b >
struct algorithm_selector {

    template< typename T >
    static void implementation( T& object )
    {
        //implement the alorithm operating on "object" here
    }
};
Next the a specialisation of algorithm_selector is added which, in this case, passes the responsability for implementing the algorithm back to the author of the object being operated on, but could well implement a second version of the operation itself.
template<>
struct algorithm_selector< true > {

    template< typename T >
    static void implementation( T& object )
    {
        object.optimised_implementation();
    }
};
Then we write the generic function that the end user of your algorithm will call, note that it in turn calls algorithm_selector, parameterised using our supports_optimised_implementation traits class:
template< typename T >
void algorithm( T& object )
{
    algorithm_selector< supports_optimised_implementation< T >::value >::implementation(object);
}
Now all that's left to do is test it against a class that doesn't support the feature ( class ObjectA{}; ), and a class that does:
class ObjectB
{
public:
    void optimised_implementation()
    {
        //...
    }
};

//specialisation of supports_optimised_implementation trait for ObjectB
template<>
struct supports_optimised_implementation< ObjectB > {
    static const bool value = true;
};
Finally, instantiate the templates:
int main(int argc, char* argv[])
{
    ObjectA a;
    algorithm( a ); // calls default implementation

    ObjectB b;
    algorithm( b ); // calls ObjectB::optimised_implementation();

    return 0;
}
[ Link: C++ Traits Example Source Code in full. ]

And that's it. Hopefully you can now "wow" your friends and colleague with your in-depth understanding of the c++ traits concept. :)

Notes

It should be noted that the examples in this article require a cutting edge, standard compliant, compiler. For example, MSVC++ 6.0 does not support static constants, and will balk on:

static const bool value = false;
This particular problem can be worked around by using an enum in its place, ie:
enum { value = false };
Please contact your compiler vendor for appropriate work arounds and bug fixes for your platform.

By Thad
With thanks to Gavin, Wookie, Russ & Francis.
(c) 2001 T.Frogley

References & further reading

  1. Traits: a new and useful template technique by Nathan C. Myers.
  2. Using C++ Trait Classes for Scientific Computing by Todd Veldhuizen.
  3. boost.org's type_traits.
  4. C++ Type Traits by John Maddock & Steve Cleary, Dr Dobb's Journal #317
  5. Traits: The else-if-then of Types - Andrei Alexandrescu
  6. Traits on Steroids - Andrei Alexandrescu
  7. Guru of the Week #71 Inheritance Traits? - Herb Sutter

Sponsors

Voxel dot net
o Managed Hosting
o VoxCAST Content Delivery
o Raw Infrastructure

Login

Poll
C++ traits
o Cool 51%
o Suck 9%
o I don't understand 19%
o I don't care 19%

Votes: 41
Results | Other Polls

Related Links
o templates
o Bjarne Stroustrup
o limits.h
o float.h
o numeric_li mits
o boost.org
o specialisa tion
o partial specialisation
o C++ Traits Example Source Code
o Traits: a new and useful template technique
o Using C++ Trait Classes for Scientific Computing
o type_trait s
o Dr Dobb's Journal
o Traits: The else-if-then of Types
o Traits on Steroids
o Inheritanc e Traits?
o Also by codemonkey_uk


Display: Sort:
Programming Tips: 2) An introduction to C++ Traits | 13 comments (9 topical, 4 editorial, 0 hidden)
char traits and basic_string (5.00 / 2) (#5)
by ucblockhead on Thu Mar 01, 2001 at 11:28:27 AM EST

It is probably worth mentioning that if you want to do fancy things with strings, like change the sorting algorithm, you need to know this stuff.

std::string is defined like:

typedef basic_string<char> string;

But because of default arguments, this actually ends up as:

typedef basic_string<char>, char_traits<char> > string;

In order to handle unicode strings, you use:

typedef basic_string<wchar_t>, char_traits<wchar_t> > wstring;

Since wchar_t is generally just unsigned short, you can see that all the work is in that char_traits.

The upshot of this is that the only difference between ASCII strings and Unicode strings is the char traits structure. The actual string code is identical.

It would be fairly simple to, say, create EBCDIC strings by creating EBCDIC char traits, using your own version of char_traits::compare and then adding the right typedef.

This is one of the underutilized parts of C++. Not only do you get a string class, but you get a generic string class that can handle strings of virtually any type.


-----------------------
This is k5. We're all tools - duxup

A caveat emptor. . . . (3.00 / 1) (#6)
by spcmanspiff on Thu Mar 01, 2001 at 04:38:38 PM EST

Yes, traits are cool.

Yes, they're useful.

But I really haven't felt the need to run out and use them yet. As long as I'm not working in some crazy-optimized critical loop or oft-called piece of code, OOP and virtual functions take care of a lot of the same functionality as C++ traits. And, for me anyway, OOP is a lot more conceptually clean than traits. In other words, a nice litle class heirarchy with a bit of inheritance and polymorphism is a lot easier for me to maintain/understand than a complicated traits system. From past experience I've seen that overuse of templates, just like overuse of OOP, leads quickly to spaghetti code.

To use the 'optimized method' example, as long as you're willing to deal with the overhead of virtual methods, it's dirt simple to do:
class normal { virtual void foo() {/* do stuff */ }; };
class optimized : public normal { virtual void foo() {/* do optimized stuff */ }; };

Saying that 'OOP dosen't help' is silly. OOP is great, as long as you can live with the extra memory / speed hit (which isn't huge, either.)


Books on like subjects? (3.00 / 1) (#10)
by Denor on Fri Mar 02, 2001 at 04:49:21 PM EST

I've come to a conclusion regarding my knowledge of C++. Either:

  • My book isn't very good, or
  • C++ has changed a lot since I first learned it (5 years or so ago) and I haven't kept up.

I think it may be a combination of both. While I learned templates in class, the STL hadn't yet been "standardized" (I think - this is from memory, so it may not be reliable). While I've picked up the STL recently, I still don't know about C++ strings and the other benefits of the stdc++ library.

This leads me to my subject: Are there any good books on the subject? I'd rather not buy an all-new "Learn C++" book, as I know most/all of the basics, preferring instead a sort of "Advanced C++" book, which would start out with classes (which I know, but have forgotten the little details of) and have all the information about the STL, string classes, traits, etc.

Or, failing that, a comprehensive guide to the language would probably work as well :)


-Denor


Pointer traits (4.50 / 2) (#11)
by bertok on Sat Mar 03, 2001 at 05:19:41 AM EST

I use the traits pattern to implement several advanced pointer types. I quickly realised that the standard auto_ptr type is too limited for general use. It cannot handle arrays, or reference counted types like COM objects. I've come up with a similar class that uses a pointer traits structure with the following interface:



struct Traits {
    static T*   acquire( T*& );
    static void release( T* );
};


The two functions correspond to the reference count
increment and reference count decrement, but can also perform operations like
copy-on-acquire, and destructive copy like the original auto_ptr class, or even
a straight pointer. The pointer class itself looks something like:



template<typename T typename Tr typename>>
class AdvP {
protected:
    T*          m_ptr;

public:
    typedef T   element_type;
    typedef Tr  traits_type;
          
public:
    template<typename U> explicit   
    AdvP( U* ptr )  { m_ptr = Tr::acquire( (U*)ptr ); }
    ~AdvP()         { Tr::release( m_ptr ); }
    ...
};

I think that the biggest advantage is that I can use the
same class to refer to C++ objects, C malloc blocks, COM objects,
and win32 handles in an exception safe or even thread safe manner with
a minimal of coding. For example, the traits for COM interfaces pointers is:




struct TrIUnknown
{
    template<typename T> static T*    acquire( T* ptr ) { if ( ptr ) ptr->AddRef(); return ptr; }    
    template<typename T> static void  release( T* ptr ) { if ( ptr ) ptr->Release(); }
};





--
"If you would be a real seeker after truth, it is necessary that at least
once in your life you doubt, as far as possible, all things."

Programming Tips: 2) An introduction to C++ Traits | 13 comments (9 topical, 4 editorial, 0 hidden)
Display: Sort:

kuro5hin.org

[XML]
All trademarks and copyrights on this page are owned by their respective companies. The Rest © 2000 - Present Kuro5hin.org Inc.
See our legalese page for copyright policies. Please also read our Privacy Policy.
Kuro5hin.org is powered by Free Software, including Apache, Perl, and Linux, The Scoop Engine that runs this site is freely available, under the terms of the GPL.
Need some help? Email help@kuro5hin.org.
My heart's the long stairs.

Powered by Scoop create account | help/FAQ | mission | links | search | IRC | YOU choose the stories!