Jump to content

  • Log In with Google      Sign In   
  • Create Account


#Actuall0k0

Posted 23 July 2013 - 03:08 AM

We use templates for a lot of different things.  I am going to (mostly) focus on how templates can prevent you from making errors in your code.  I think this is a frequently overlooked when people considers templates.

 

1) Data Structures.  Just because the STL provides some doesn't mean it has all of the data structures you need.  In addition, very few commercial game companies use the STL (and you should atleast know why).  One simple one that you could write would be a range checking array.  If the operator [] functions are inlined a good compiler will optimize it out completely.  However, in debug builds you can throw in an assert that will catch these out of range errors that will save you a lot of debugging time.  To get you started:

template <typename TYPE, int ARRAY_SIZE>
class RangeArray {
public:
     STATIC_ASSERT(ARRAY_SIZE > 0);
     TYPE& operator[](int idx) { Assert(idx >= 0 && idx < ARRAY_SIZE); return m_array[idx]; }
private:
    TYPE m_array[ARRAY_SIZE];
};
RangeArray<int, 5> myArray;
printf("%d\n", myArray[0]);
int someIndex = 2 * 2 * 2;
int i = myArray[someIndex]; // this will compile, but it will not fail silently at runtime

2) Static assertions are very useful for catching bad code at compile time.  If you want to limit the overloads to a certain type by providing all of the possible overloads and throwing a static assertion into the main templated function.  Or not even define it at all, just declare it.  If you can catch the error at compile time it is a lot better than at runtime.  Especially since assertions often aren't of the fatal variety if you are working on a large project.  Here's a somewhat practical example, with a Vector3.  The range and size of the structure is known and constant, so we can use non type template parameters.

class Vector3 {
public:
    // If the index is not 0, 1, or 2 it is known to be invalid!
    template <int idx> float getElem() const { STATIC_ASSERT_FAIL_MSG("Invalid element index!"); } 
    template <> float getElem<0>() const { return x; }
    template <> float getElem<1>() const { return y; }
    template <> float getElem<2>() const { return z; }
private:
    float x, y, z;
};
Vector3 someVec;
float atElem0 = someVec.getElem<0>(); // OK
float atElem4 = someVec.getElem<5>(); // ERROR will not compile

3) Compilers can inline templates and the optimizations made possible with inlining can be quite remarkable.  It is what makes them superior to function pointers (and is largely why std::sort trumps qsort).  It's also extremely useful in the form of delegates and functors.  Even if you use the stl, you will need to define your own functors all the time to process your custom data.  

 

4) Strings.  It is a pain to support code that works with 8 bit, 16 bit, and 32 bit characters.  If you ever embark on localization or code that otherwise needs to support both at runtime the only sane way to do it (in my opinion anyways) would be with templates.  Certainly better than using the preprocessor all over the place to route it to the equivalent call.  

 

In addition, I frequently don't really know how large statically sized arrays should be when I do string concatenations on c-strings.  A frequently unknown benefit of templates is that they can take the size of the array as a parameter.  Alloca won't work here because we can't get the string sizes if they aren't constructed yet.

template <typename CHAR_TYPE, int ARRAY_SIZE>
INLINE CHAR_TYPE * string_format(CHAR_TYPE ( & dest) [ARRAY_SIZE], const CHAR_TYPE * fmt, ...) {
    va_list argp;
    ASSERT_ONLY(int numWritten =) vsprintf(dest, fmt, argp); // yes I know this will only work with char and not wchar_t...
    SLOW_ASSERT_MSG(numWritten < ARRAY_SIZE && numWritten > 0, "Array not big enough!");
    va_end(argp);
    return dest;
}
const int kStrArraySize = 256;
char strArray[kStrArraySize];
string_format(strArray, "Entity Position: %f, %f, %f\n", entPos.x, entPos.y, entPos.z);
DebugDrawString(entPos, strArray);
// if we do a bunch of string formatting on this array and make debug drawing calls and it overflows, we can // go back and change it in ONE place.  We don't have to find all of the places we passed kStrArraySize and // change that too.

This will catch an array overrun without silently thrashing memory.  Or at least without you having to check the return value on vsprintf every time yourself.

 

5) Catching errors in casting pointer types.  This can be a real lifesaver when you have a message/event system that uses base ptr data.   dynamic_cast is almost always a sign of bad design, but don't underestimate your ability to make a mistake when casting.  NOTE: dynamic_cast requires the from type has a vtable!


// this code assumes LPTR_TYPE and RPTR_TYPE are pointer types, although there is a template for that too :)
template <typename LPTR_TYPE, typename RPTR_TYPE> 
FORCE_INLINE LPTR_TYPE smart_cast(RPTR_TYPE right) { 
#if _CPPRTTI 
    Assert(!right || dynamic_cast<LPTR_TYPE>(right)); 
#endif 
    return static_cast<LPTR_TYPE>(right); 
}

This was extremely useful for pool allocated data seen in message passing and event systems.

 

6)   Also great when making assignments between numerical types with different bits of precision.  For example, lets say we have a U32 but need to pass it as U16.  

template <typename LTYPE, typename RTYPE>
LTYPE& Assign(LTYPE& assignedTo, const RTYPE assigned) {
    assignedTo = assigned;
    Assert(assignedTo == assigned); // this will fail if there weren't enough bits in LTYPE to store RTYPE
    return assignedTo;
}
   // example code that isn't really practical
   U32 src = U16_MAX + 5;
   U16 dest = 0; 
   Assign(dest, src); // this will not fail silently anymore

7) Other things I have found templates to be useful for.

    - Component Entity Systems

    - Your own RTTI system (this will usually require the preprocessor, a virtual function, and a hash code too)

    - Permutations/Shuffling in vector libraries

    - Some of my coworkers love it for serialization code.  That's not really something I can say I'm an expert on but leaving it here anyways.

    - Static Assertions themselves

    - All that metaprogramming stuff

    - enum range checking at compile time

    - avoiding virtual functions

    - a bunch of other things I haven't mentioned...

 

But don't underestimate how much time it will save you from the errors it can catch in your code.  Most of this should compile out cleanly in release builds!


#4l0k0

Posted 10 May 2013 - 02:12 AM

We use templates for a lot of different things.  I am going to (mostly) focus on how templates can prevent you from making errors in your code.  I think this is a frequently overlooked when people considers templates.

 

1) Data Structures.  Just because the STL provides some doesn't mean it has all of the data structures you need.  In addition, very few commercial game companies use the STL (and you should atleast know why).  One simple one that you could write would be a range checking array.  If the operator [] functions are inlined a good compiler will optimize it out completely.  However, in debug builds you can throw in an assert that will catch these out of range errors that will save you a lot of debugging time.  To get you started:

 

template <typename TYPE, int ARRAY_SIZE>
class RangeArray {
public:
     STATIC_ASSERT(ARRAY_SIZE > 0);
     TYPE& operator[](int idx) { Assert(idx >= 0 && idx < ARRAY_SIZE); return m_array[idx]; }
private:
    TYPE m_array[SIZE];
};

 

RangeArray<int, 5> myArray;
printf("%d\n", myArray[0]);
int someIndex = 2 * 2 * 2;
int i = myArray[someIndex]; // this will compile, but it will not fail silently at runtime

 

2) Static assertions are very useful for catching bad code at compile time.  If you want to limit the overloads to a certain type by providing all of the possible overloads and throwing a static assertion into the main templated function.  Or not even define it at all, just declare it.  If you can catch the error at compile time it is a lot better than at runtime.  Especially since assertions often aren't of the fatal variety if you are working on a large project.  Here's a somewhat practical example, with a Vector3.  The range and size of the structure is known and constant, so we can use non type template parameters.

 

class Vector3 {
public:
    // If the index is not 0, 1, or 2 it is known to be invalid!
    template <int idx> float getElem() const { STATIC_ASSERT_FAIL_MSG("Invalid element index!"); } 
    template <> float getElem<0>() const { return x; }
    template <> float getElem<1>() const { return y; }
    template <> float getElem<2>() const { return z; }
private:
    float x, y, z;
};
Vector3 someVec;
float atElem0 = someVec.getElem<0>(); // OK
float atElem4 = someVec.getElem<5>(); // ERROR will not compile

 

 

3) Compilers can inline templates and the optimizations made possible with inlining can be quite remarkable.  It is what makes them superior to function pointers (and is largely why std::sort trumps qsort).  It's also extremely useful in the form of delegates and functors.  Even if you use the stl, you will need to define your own functors all the time to process your custom data.  

 

4) Strings.  It is a pain to support code that works with 8 bit, 16 bit, and 32 bit characters.  If you ever embark on localization or code that otherwise needs to support both at runtime the only sane way to do it (in my opinion anyways) would be with templates.  Certainly better than using the preprocessor all over the place to route it to the equivalent call.  

 

In addition, I frequently don't really know how large statically sized arrays should be when I do string concatenations on c-strings.  A frequently unknown benefit of templates is that they can take the size of the array as a parameter.  Alloca won't work here because we can't get the string sizes if they aren't constructed yet.

 

template <typename CHAR_TYPE, int ARRAY_SIZE>
INLINE CHAR_TYPE * string_format(CHAR_TYPE dest[SIZE], const CHAR_TYPE * fmt, ...) {
    va_list argp;
    ASSERT_ONLY(int numWritten =) vsprintf(dest, fmt, argp); // yes I know this will only work with char and not wchar_t...
    SLOW_ASSERT_MSG(numWritten < ARRAY_SIZE && numWritten > 0, "Array not big enough!");
    va_end(argp);
    return dest;
}
const int kStrArraySize = 256;
char strArray[kStrArraySize];
string_format(strArray, "Entity Position: %f, %f, %f\n", entPos.x, entPos.y, entPos.z);
DebugDrawString(entPos, strArray);
// if we do a bunch of string formatting on this array and make debug drawing calls and it overflows, we can // go back and change it in ONE place.  We don't have to find all of the places we passed kStrArraySize and // change that too.

 

This will catch an array overrun without silently thrashing memory.  Or at least without you having to check the return value on vsprintf every time yourself.

 

5) Catching errors in casting pointer types.  This can be a real lifesaver when you have a message/event system that uses base ptr data.   dynamic_cast is almost always a sign of bad design, but don't underestimate your ability to make a mistake when casting.  NOTE: dynamic_cast requires the from type has a vtable!


// this code assumes LPTR_TYPE and RPTR_TYPE are pointer types, although there is a template for that too :)
template <typename LPTR_TYPE, typename RPTR_TYPE> 
FORCE_INLINE LPTR_TYPE smart_cast(RPTR_TYPE right) { 
#if _CPPRTTI 
    Assert(!right || dynamic_cast<LPTR_TYPE>(right)); 
#endif 
    return static_cast<LPTR_TYPE>(right); 
}

 

This was extremely useful for pool allocated data seen in message passing and event systems.

 

6)   Also great when making assignments between numerical types with different bits of precision.  For example, lets say we have a U32 but need to pass it as U16.  

template <typename LTYPE, typename RTYPE>
LTYPE& Assign(LTYPE& assignedTo, const RTYPE assigned) {
    assignedTo = assigned;
    Assert(assignedTo == assigned); // this will fail if there weren't enough bits in LTYPE to store RTYPE
    return assignedTo;
}
   // example code that isn't really practical
   U32 src = U16_MAX + 5;
   U16 dest = 0; 
   Assign(dest, src); // this will not fail silently anymore

 

 

7) Other things I have found templates to be useful for.

    - Component Entity Systems

    - Your own RTTI system (this will usually require the preprocessor, a virtual function, and a hash code too)

    - Permutations/Shuffling in vector libraries

    - Some of my coworkers love it for serialization code.  That's not really something I can say I'm an expert on but leaving it here anyways.

    - Static Assertions themselves

    - All that metaprogramming stuff

    - enum range checking at compile time

    - avoiding virtual functions

    - a bunch of other things I haven't mentioned...

 

But don't underestimate how much time it will save you from the errors it can catch in your code.  Most of this should compile out cleanly in release builds!


#3l0k0

Posted 07 January 2013 - 12:41 AM

We use templates for a lot of different things.  I am going to (mostly) focus on how templates can prevent you from making errors in your code.  I think this is a frequently overlooked when people considers templates.

 

1) Data Structures.  Just because the STL provides some doesn't mean it has all of the data structures you need.  In addition, very few commercial game companies use the STL (and you should atleast know why).  One simple one that you could write would be a range checking array.  If the operator [] functions are inlined a good compiler will optimize it out completely.  However, in debug builds you can throw in an assert that will catch these out of range errors that will save you a lot of debugging time.  To get you started:

 

template <typename TYPE, int ARRAY_SIZE>
class RangeArray {
public:
     STATIC_ASSERT(ARRAY_SIZE > 0);
     TYPE& operator[](int idx) { Assert(idx >= 0 && idx < ARRAY_SIZE); return m_array[idx]; }
private:
    TYPE m_array[ARRAY_SIZE];
};

 

RangeArray<int, 5> myArray;
printf("%d\n", myArray[0]);
int someIndex = 2 * 2 * 2;
int i = myArray[someIndex]; // this will compile, but it will not fail silently at runtime

 

2) Static assertions are very useful for catching bad code at compile time.  If you want to limit the overloads to a certain type by providing all of the possible overloads and throwing a static assertion into the main templated function.  Or not even define it at all, just declare it.  If you can catch the error at compile time it is a lot better than at runtime.  Especially since assertions often aren't of the fatal variety if you are working on a large project.  Here's a somewhat practical example, with a Vector3.  The range and size of the structure is known and constant, so we can use non type template parameters.

 

class Vector3 {
public:
    // If the index is not 0, 1, or 2 it is known to be invalid!
    template <int idx> float getElem() const { STATIC_ASSERT_FAIL_MSG("Invalid element index!"); } 
    template <> float getElem<0>() const { return x; }
    template <> float getElem<1>() const { return y; }
    template <> float getElem<2>() const { return z; }
private:
    float x, y, z;
};
Vector3 someVec;
float atElem0 = someVec.getElem<0>(); // OK
float atElem4 = someVec.getElem<5>(); // ERROR will not compile

 

 

3) Compilers can inline templates and the optimizations made possible with inlining can be quite remarkable.  It is what makes them superior to function pointers (and is largely why std::sort trumps qsort).  It's also extremely useful in the form of delegates and functors.  Even if you use the stl, you will need to define your own functors all the time to process your custom data.  

 

4) Strings.  It is a pain to support code that works with 8 bit, 16 bit, and 32 bit characters.  If you ever embark on localization or code that otherwise needs to support both at runtime the only sane way to do it (in my opinion anyways) would be with templates.  Certainly better than using the preprocessor all over the place to route it to the equivalent call.  

 

In addition, I frequently don't really know how large statically sized arrays should be when I do string concatenations on c-strings.  A frequently unknown benefit of templates is that they can take the size of the array as a parameter.  Alloca won't work here because we can't get the string sizes if they aren't constructed yet.

 

template <typename CHAR_TYPE, int ARRAY_SIZE>
INLINE CHAR_TYPE * string_format(CHAR_TYPE dest[ARRAY_SIZE], const CHAR_TYPE * fmt, ...) {
    va_list argp;
    ASSERT_ONLY(int numWritten =) vsprintf(dest, fmt, argp); // yes I know this will only work with char and not wchar_t...
    SLOW_ASSERT_MSG(numWritten < ARRAY_SIZE && numWritten > 0, "Array not big enough!");
    va_end(argp);
    return dest;
}
const int kStrArraySize = 256;
char strArray[kStrArraySize];
string_format(strArray, "Entity Position: %f, %f, %f\n", entPos.x, entPos.y, entPos.z);
DebugDrawString(entPos, strArray);
// if we do a bunch of string formatting on this array and make debug drawing calls and it overflows, we can // go back and change it in ONE place.  We don't have to find all of the places we passed kStrArraySize and // change that too.

 

This will catch an array overrun without silently thrashing memory.  Or at least without you having to check the return value on vsprintf every time yourself.

 

5) Catching errors in casting pointer types.  This can be a real lifesaver when you have a message/event system that uses void * data.   dynamic_cast is almost always a sign of bad design, but don't underestimate your ability to make a mistake when casting.


// this code assumes LPTR_TYPE and RPTR_TYPE are pointer types, although there is a template for that too :)
template <typename LPTR_TYPE, typename RPTR_TYPE> 
FORCE_INLINE LPTR_TYPE smart_cast(RPTR_TYPE right) { 
#if _CPPRTTI 
    Assert(!right || dynamic_cast<LPTR_TYPE>(right)); 
#endif 
    return static_cast<LPTR_TYPE>(right); 
}

 

This was extremely useful for pool allocated void * data seen in message passing and event systems.

 

6)   Also great when making assignments between numerical types with different bits of precision.  For example, lets say we have a U32 but need to pass it as U16.  

template <typename LTYPE, typename RTYPE>
LTYPE& Assign(LTYPE& assignedTo, const RTYPE assigned) {
    assignedTo = assigned;
    Assert(assignedTo == assigned); // this will fail if there weren't enough bits in LTYPE to store RTYPE
    return assignedTo;
}
   // example code that isn't really practical
   U32 src = U16_MAX + 5;
   U16 dest = 0; 
   Assign(dest, src); // this will not fail silently anymore

 

 

7) Other things I have found templates to be useful for.

    - Component Entity Systems

    - Your own RTTI system (this will usually require the preprocessor, a virtual function, and a hash code too)

    - Permutations/Shuffling in vector libraries

    - Some of my coworkers love it for serialization code.  That's not really something I can say I'm an expert on but leaving it here anyways.

    - Static Assertions themselves

    - All that metaprogramming stuff

    - enum range checking at compile time

    - avoiding virtual functions

    - a bunch of other things I haven't mentioned...

 

But don't underestimate how much time it will save you from the errors it can catch in your code.  Most of this should compile out cleanly in release builds!


#2l0k0

Posted 06 January 2013 - 09:37 PM

We use templates for a lot of different things.  I am going to (mostly) focus on how templates can prevent you from making errors in your code.  I think this is a frequently overlooked when people considers templates.

 

1) Data Structures.  Just because the STL provides some doesn't mean it has all of the data structures you need.  In addition, very few commercial game companies use the STL (and you should atleast know why).  One simple one that you could write would be a range checking array.  If the operator [] functions are inlined a good compiler will optimize it out completely.  However, in debug builds you can throw in an assert that will catch these out of range errors that will save you a lot of debugging time.  To get you started:

 

template <typename TYPE, int ARRAY_SIZE>
class RangeArray {
public:
     STATIC_ASSERT(ARRAY_SIZE > 0);
     TYPE& operator[](int idx) { Assert(idx >= 0 && idx < ARRAY_SIZE); return m_array[idx]; }
private:
    TYPE m_array[ARRAY_SIZE];
};

 

RangeArray<int, 5> myArray;
printf("%d\n", myArray[0]);
int someIndex = 2 * 2 * 2;
int i = myArray[someIndex]; // this will compile, but it will not fail silently at runtime

 

2) Static assertions are very useful for catching bad code at compile time.  If you want to limit the overloads to a certain type by providing all of the possible overloads and throwing a static assertion into the main templated function.  Or not even define it at all, just declare it.  If you can catch the error at compile time it is a lot better than at runtime.  Especially since assertions often aren't of the fatal variety if you are working on a large project.  Here's a somewhat practical example, with a Vector3.  The range and size of the structure is known and constant, so we can use non type template parameters.

 

class Vector3 {
public:
    // If the index is not 0, 1, or 2 it is known to be invalid!
    template <int idx> float getElem() const { STATIC_ASSERT_FAIL_MSG("Invalid element index!"); } 
    template <> float getElem<0>() const { return x; }
    template <> float getElem<1>() const { return y; }
    template <> float getElem<2>() const { return z; }
private:
    float x, y, z;
};
Vector3 someVec;
float atElem0 = someVec.getElem<0>(); // OK
float atElem4 = someVec.getElem<5>(); // ERROR will not compile

 

 

3) Compilers can inline templates and the optimizations made possible with inlining can be quite remarkable.  It is what makes them superior to function pointers (and is largely why std::sort trumps qsort).  It's also extremely useful in the form of delegates and functors.  Even if you use the stl, you will need to define your own functors all the time to process your custom data.  

 

4) Strings.  It is a pain to support code that works with 8 bit, 16 bit, and 32 bit characters.  If you ever embark on localization or code that otherwise needs to support both at runtime the only sane way to do it (in my opinion anyways) would be with templates.  Certainly better than using the preprocessor all over the place to route it to the equivalent call.  

 

In addition, I frequently don't really know how large statically sized arrays should be when I do string concatenations on c-strings.  A frequently unknown benefit of templates is that they can take the size of the array as a parameter.  Alloca won't work here because we can't get the string sizes if they aren't constructed yet.

 

template <typename CHAR_TYPE, int ARRAY_SIZE>
INLINE CHAR_TYPE * string_format(CHAR_TYPE dest[ARRAY_SIZE], const CHAR_TYPE * fmt, ...) {
    va_list argp;
    ASSERT_ONLY(int numWritten =) vsprintf(dest, fmt, argp); // yes I know this will only work with char and not wchar_t...
    SLOW_ASSERT_MSG(numWritten < ARRAY_SIZE && numWritten > 0, "Array not big enough!");
    va_end(argp);
    return dest;
}
const int kStrArraySize = 256;
char strArray[kStrArraySize];
string_format(strArray, "Entity Position: %f, %f, %f\n", entPos.x, entPos.y, entPos.z);
DebugDrawString(entPos, strArray);
// if we do a bunch of string formatting on this array and make debug drawing calls and it overflows, we can // go back and change it in ONE place.  We don't have to find all of the places we passed kStrArraySize and // change that too.

 

This will catch an array overrun without silently thrashing memory.  Or at least without you having to check the return value on vsprintf every time yourself.

 

5) Catching errors in casting pointer types.  This can be a real lifesaver when you have a message/event system that uses void * data.   dynamic_cast is almost always a sign of bad design, but don't underestimate your ability to make a mistake when casting.


// this code assumes LPTR_TYPE and RPTR_TYPE are pointer types, although there is a template for that too :)
template <typename LPTR_TYPE, typename RPTR_TYPE> 
FORCE_INLINE LPTR_TYPE * smart_cast(LPTR_TYPE left, RPTR_TYPE right) { 
#if _CPPRTTI 
    Assert(!right || dynamic_cast<LPTR_TYPE>(right)); 
#endif 
    return static_cast<LPTR_TYPE>(right); 
}

 

This was extremely useful for pool allocated void * data seen in message passing and event systems.

 

6)   Also great when making assignments between numerical types with different bits of precision.  For example, lets say we have a U32 but need to pass it as U16.  

template <typename LTYPE, typename RTYPE>
LTYPE& Assign(LTYPE& assignedTo, const RTYPE assigned) {
    assignedTo = assigned;
    Assert(assignedTo == assigned); // this will fail if there weren't enough bits in LTYPE to store RTYPE
    return assignedTo;
}
   // example code that isn't really practical
   U32 src = U16_MAX + 5;
   U16 dest = 0; 
   Assign(dest, src); // this will not fail silently anymore

 

 

7) Other things I have found templates to be useful for.

    - Component Entity Systems

    - Your own RTTI system (this will usually require the preprocessor, a virtual function, and a hash code too)

    - Permutations/Shuffling in vector libraries

    - Some of my coworkers love it for serialization code.  That's not really something I can say I'm an expert on but leaving it here anyways.

    - Static Assertions themselves

    - All that metaprogramming stuff

    - enum range checking at compile time

    - avoiding virtual functions

    - a bunch of other things I haven't mentioned...

 

But don't underestimate how much time it will save you from the errors it can catch in your code.  Most of this should compile out cleanly in release builds!


#1l0k0

Posted 06 January 2013 - 08:11 PM

We use templates for a lot of different things.  I am going to (mostly) focus on how templates can prevent you from making errors in your code.  I think this is a frequently overlooked when people considers templates.

 

1) Data Structures.  Just because the STL provides some doesn't mean it has all of the data structures you need.  In addition, very few commercial game companies use the STL (and you should atleast know why).  One simple one that you could write would be a range checking array.  If the operator [] functions are inlined a good compiler will optimize it out completely.  However, in debug builds you can throw in an assert that will catch these out of range errors that will save you a lot of debugging time.  To get you started:

 

template <typename TYPE, int ARRAY_SIZE>
class RangeArray {
public:
     STATIC_ASSERT(ARRAY_SIZE > 0);
     TYPE& operator[](int idx) { Assert(idx >= 0 && idx < ARRAY_SIZE); return m_array[idx]; }
private:
    TYPE m_array[ARRAY_SIZE];
};

 

RangeArray<int, 5> myArray;
printf("%d\n", myArray[0]);
int someIndex = 2 * 2 * 2;
int i = myArray[someIndex]; // this will compile, but it will not fail silently at runtime

 

2) Static assertions are very useful for catching bad code at compile time.  If you want to limit the overloads to a certain type by providing all of the possible overloads and throwing a static assertion into the main templated function.  Or not even define it at all, just declare it.  If you can catch the error at compile time it is a lot better than at runtime.  Especially since assertions often aren't of the fatal variety if you are working on a large project.  Here's a somewhat practical example, with a Vector3.  The range and size of the structure is known and constant, so we can use non type template parameters.

 

class Vector3 {
public:
    // If the index is not 0, 1, or 2 it is known to be invalid!
    template <int idx> float getElem() const { STATIC_ASSERT_FAIL_MSG("Invalid element index!"); } 
    template <> float getElem<0>() const { return x; }
    template <> float getElem<1>() const { return y; }
    template <> float getElem<2>() const { return z; }
private:
    float x, y, z;
};
Vector3 someVec;
float atElem0 = someVec.getElem<0>(); // OK
float atElem4 = someVec.getElem<5>(); // ERROR will not compile

 

 

3) Compilers can inline templates and the optimizations made possible with inlining can be quite remarkable.  It is what makes them superior to function pointers (and is largely why std::sort trumps qsort).  It's also extremely useful in the form of delegates and functors.  Even if you use the stl, you will need to define your own functors all the time to process your custom data.  

 

4) Strings.  It is a pain to support code that works with 8 bit, 16 bit, and 32 bit characters.  If you ever embark on localization or code that otherwise needs to support both at runtime the only sane way to do it (in my opinion anyways) would be with templates.  Certainly better than using the preprocessor all over the place to route it to the equivalent call.  

 

In addition, I frequently don't really know how large statically sized arrays should be when I do string concatenations on c-strings.  A frequently unknown benefit of templates is that they can take the size of the array as a parameter.  Alloca won't work here because we can't get the string sizes if they aren't constructed yet.

 

template <typename CHAR_TYPE, int ARRAY_SIZE>
INLINE CHAR_TYPE * string_format(CHAR_TYPE dest[ARRAY_SIZE], const CHAR_TYPE * fmt, ...) {
    va_list argp;
    ASSERT_ONLY(int numWritten =) vsprintf(dest, fmt, argp); // yes I know this will only work with char and not wchar_t...
    SLOW_ASSERT_MSG(numWritten < ARRAY_SIZE && numWritten > 0, "Array not big enough!");
    va_end(argp);
    return dest;
}
const int kStrArraySize = 256;
char strArray[kStrArraySize];
string_format(strArray, "Entity Position: %f, %f, %f\n", entPos.x, entPos.y, entPos.z);
DebugDrawString(entPos, strArray);
// if we do a bunch of string formatting on this array and make debug drawing calls and it overflows, we can // go back and change it in ONE place.  We don't have to find all of the places we passed kStrArraySize and // change that too.

 

This will catch an array overrun without silently thrashing memory.  Or at least without you having to check the return value on vsprintf every time yourself.

 

5) Catching errors in casting pointer types.  This can be a real lifesaver when you have a message/event system that uses void * data.   dynamic_cast is almost always a sign of bad design, but don't underestimate your ability to make a mistake when casting.


// this code assumes LPTR_TYPE and RPTR_TYPE are pointer types, although there is a template for that too :)
template <typename LPTR_TYPE, typename RPTR_TYPE> 
FORCE_INLINE LPTR_TYPE * smart_cast(LPTR_TYPE left, RPTR_TYPE right) { 
#if _CPPRTTI 
    Assert(!right || dynamic_cast<LPTR_TYPE>(right)); 
#endif 
    return static_cast<LPTR_TYPE>(right); 
}

 

This was extremely useful for pool allocated void * data seen in message passing and event systems.

 

6)   Also great when making assignments between numerical types with different bits of precision.  For example, lets say we have a U32 but need to pass it as U16.  

template <typename LTYPE, typename RTYPE>
LTYPE& Assign(const LTYPE& assignedTo, const RTYPE assigned) {
    assignedTo = assigned;
    Assert(assignedTo == assigned); // this will fail if there weren't enough bits in LTYPE to store RTYPE
    return assignedTo;
}
   // example code that isn't really practical
   U32 src = U16_MAX + 5;
   U16 dest = 0; 
   Assign(dest, src); // this will not fail silently anymore

 

 

7) Other things I have found templates to be useful for.

    - Component Entity Systems

    - Your own RTTI system (this will usually require the preprocessor, a virtual function, and a hash code too)

    - Permutations/Shuffling in vector libraries

    - Some of my coworkers love it for serialization code.  That's not really something I can say I'm an expert on but leaving it here anyways.

    - Static Assertions themselves

    - All that metaprogramming stuff

    - enum range checking at compile time

    - avoiding virtual functions

    - a bunch of other things I haven't mentioned...

 

But don't underestimate how much time it will save you from the errors it can catch in your code.  Most of this should compile out cleanly in release builds!


PARTNERS