Tuesday, January 18, 2011

Expression parameters and template specialization

In previous lessons, you’ve learned how to use template type parameters to create functions and classes that are type independent. However, template type parameters are not the only type of template parameters available. Template classes (not template functions) can make use of another kind of template parameter known as an expression parameter.
Expression parameters
A template expression parameter is a parameter that does not substitute for a type, but is instead replaced by a value. An expression parameter can be any of the following:
  • A value that has an integral type or enumeration
  • A pointer or reference to an object
  • A pointer or reference to a function
  • A pointer or reference to a class member function
In the following example, we create a buffer class that uses both a type parameter and an expression parameter. The type parameter controls the data type of the buffer array, and the expression parameter controls how large the buffer array is.
01template <typename T, int nSize> // nSize is the expression parameter
02class Buffer
03{
04private:
05    // The expression parameter controls the size of the array
06    T m_atBuffer[nSize];
07 
08public:
09    T* GetBuffer() { return m_atBuffer; }
10 
11    T& operator[](int nIndex)
12    {
13        return m_atBuffer[nIndex];
14    }
15};
16 
17int main()
18{
19    // declare an integer buffer with room for 12 chars
20    Buffer<int, 12> cIntBuffer;
21 
22    // Fill it up in order, then print it backwards
23    for (int nCount=0; nCount < 12; nCount++)
24        cIntBuffer[nCount] = nCount;
25 
26    for (int nCount=11; nCount >= 0; nCount--)
27        std::cout << cIntBuffer[nCount] << " ";
28    std::cout << std::endl;
29 
30    // declare a char buffer with room for 31 chars
31    Buffer<char, 31> cCharBuffer;
32 
33    // strcpy a string into the buffer and print it
34    strcpy(cCharBuffer.GetBuffer(), "Hello there!");
35    std::cout << cCharBuffer.GetBuffer() << std::endl;
36 
37    return 0;
38}
This code produces the following:
11 10 9 8 7 6 5 4 3 2 1 0
Hello there!
One noteworthy thing about the above example is that we do not have to dynamically allocate the m_atBuffer member array! This is because for any given instance of the Buffer class, nSize is actually constant. For example, if you instantiate a Buffer, the compiler replaces nSize with 12. Thus m_atBuffer is of type int[12], which can be allocated statically.
Template specialization
When instantiating a template class for a given type, the compiler stencils out a copy of each templated member function, and replaces the template type parameters with the actual types used in the variable declaration. This means a particular member function will have the same implementation details for each instanced type. While most of the time, this is exactly what you want, occasionally there are cases where it is useful to implement a templated member function slightly different for a specific data type. Template specialization lets you accomplish exactly this.
Let’s take a look at a very simple example:
01using namespace std;
02 
03template <typename T>
04class Storage
05{
06private:
07    T m_tValue;
08public:
09    Storage(T tValue)
10    {
11         m_tValue = tValue;
12    }
13 
14    ~Storage()
15    {
16    }
17 
18    void Print()
19    {
20        std::cout << m_tValue << std::endl;;
21    }
22};
The above code will work fine for many data types:
01int main()
02{
03    // Define some storage units
04    Storage<int> nValue(5);
05    Storage<double> dValue(6.7);
06 
07    // Print out some values
08    nValue.Print();
09    dValue.Print();
10}
This prints:
5
6.7
Now, let’s say we want double values to output in scientific notation. To do so, we will need to use template specialization to create a specialized version of the Print() function for doubles. This is extremely simple: simply define the specialized function outside of the class definition, replacing the template type with the specific type you wish to redefine the function for. Here is our specialized Print() function for doubles:
1void Storage<double>::Print()
2{
3    std::cout << std::scientific << m_tValue << std::endl;
4}
When the compiler goes to instantiate Storage<double>::Print(), it will see we’ve already defined one, and it will use the one we’ve defined instead of stenciling out a version from the generic templated member function.
As a result, when we rerun the above program, it will print:
5
6.700000e+000
Now let’s take a look at another example where template specialization can be useful. Consider what happens if we try to use our templated Storage class with datatype char*:
01int main()
02{
03    using namespace std;
04 
05    // Dynamically allocate a temporary string
06    char *strString = new char[40];
07 
08    // Ask user for their name
09    cout << "Enter your name: ";
10    cin >> strString;
11 
12    // Store the name
13    Storage<char*> strValue(strString);
14 
15    // Delete the temporary string
16    delete strString;
17 
18    // Print out our value
19    strValue.Print(); // This will print garbage
20}
As it turns out, instead of printing the name the user input, strValue.Print() prints garbage! What’s going on here?
When Storage is instantiated for type char*, the constructor for Storage<char*> looks like this:
1Storage<char*>::Storage(char* tValue)
2{
3     m_tValue = tValue;
4}
In other words, this just does a pointer assignment! As a result, m_tValue ends up pointing at the same memory location as strString. When we delete strString in main(), we end up deleting the value that m_tValue was pointing at! And thus, we get garbage when trying to print that value.
Fortunately, we can fix this problem using template specialization. Instead of doing a pointer copy, we’d really like our constructor to make a copy of the input string. So let’s write a specialized constructor for datatype char* that does exactly that:
1Storage<char*>::Storage(char* tValue)
2{
3    // Allocate memory to hold the tValue string
4    m_tValue = new char[strlen(tValue)+1];
5    // Copy the actual tValue string into the m_tValue memory we just allocated
6    strcpy(m_tValue, tValue);
7}
Now when we allocate a variable of type Storage<char*>, this constructor will get used instead of the default one. As a result, m_tValue will receive its own copy of strString. Consequently, when we delete strString, m_tValue will be unaffected.
However, this class now has a memory leak for type char*, because m_tValue will not be deleted when the Storage variable goes out of scope. As you might have guessed, this can also be solved by specializing the Storage<char*> destructor:
1Storage<char*>::~Storage()
2{
3    delete[] m_tValue;
4}
Now when variables of type ~Storage<char*> go out of scope, the memory allocated in the specialized constructor will be deleted in the specialized destructor.

No comments: