Tuesday, January 18, 2011

Partial template specialization

In the lesson on expression parameters and template specialization, you learned how expression parameters could be used to parametrize template classes.
Let’s take another look at the Buffer class we used in the previous example:
01template <typename T, int nSize> // nSize is the expression parameter
02class Buffer
03{
04private:
05    // The expression parameter controls the side 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 a char buffer
20    Buffer<char, 10> cChar10Buffer;
21 
22    // copy a value into the buffer
23    strcpy(cChar10Buffer.GetBuffer(), "Ten");
24 
25    return 0;
26}
Now, let’s say we wanted to write a function to print out a buffer as a string. Although we could implement this as a member function, we’re going to do it as a non-member function instead because it will make the successive examples easier to follow.
Using templates, we might write something like this:
1template <typename T, int nSize>
2void PrintBufferString(Buffer<T, nSize> &rcBuf)
3{
4    std::cout << rcBuf.GetBuffer() << std::endl;
5}
This would allow us to do the following:
01int main()
02{
03    // declare a char buffer
04    Buffer<char, 10> cChar10Buffer;
05 
06    // copy a value into the buffer
07    strcpy(cChar10Buffer.GetBuffer(), "Ten");
08 
09    // Print the value
10    PrintBufferString(cChar10Buffer);
11    return 0;
12}
and get the following result:
Ten
Although this works, it has a design flaw. Consider the following:
01int main()
02{
03    // declare an int buffer
04    Buffer<int, 10> cInt10Buffer;
05 
06    // copy values into the buffer
07    for (int nCount=0; nCount < 10; nCount++)
08        cInt10Buffer[nCount] = nCount;
09 
10    // Print the value?
11    PrintBufferString(cInt10Buffer); // what does this mean?
12    return 0;
13}
This program will compile, execute, and produce the following value (or one similar):
0012FF10
What happened? PrintBufferString() has std::cout print the value of rcBuf.GetBuffer(), which returns a pointer to m_atBuffer! When the data type is a char, cout will print the array as a C-style character string, but when the data type is non-char (such as in this case), cout will print the address that the pointer is holding!
Obviously this case exposes a misuse of this function (as written). Without explicitly examining the code, the programmer would not have any clue that this function does not handle non-char buffers correctly. This is likely to lead to programming errors.
Template specialization
One seemingly useful way to solve this problem is to use template specialization to ensure that only arrays of type char can be passed to PrintBufferString(). As you learned in the previous lesson, template specialization allows you to define a function where all of the templated types have been resolved to a specific data type.
Here’s an example of how that might work here:
01void PrintBufferString(Buffer<char, 10> &rcBuf)
02{
03    std::cout << rcBuf.GetBuffer() << std::endl;
04}
05 
06int main()
07{
08    // declare a char buffer
09    Buffer<char, 10> cChar10Buffer;
10 
11    // copy a value into the buffer
12    strcpy(cChar10Buffer.GetBuffer(), "Ten");
13 
14    // Print the value
15    PrintBufferString(cChar10Buffer);
16    return 0;
17}
As you can see, we’ve now specialized PrintBufferString so it will only accept Buffers of type char and of length 10. This means if we try to call PrintBufferString with an int buffer, the compiler will give us an error.
Although this solves the issue of making sure PrintBufferString can not be called with an int Buffer, it brings up another problem: using full template specialization means we have to explicitly define the length of the buffer this function will accept! Consider the following example:
01int main()
02{
03    Buffer<char, 10> cChar10Buffer;
04    Buffer<char, 11> cChar11Buffer;
05 
06    strcpy(cChar10Buffer.GetBuffer(), "Ten");
07    strcpy(cChar11Buffer.GetBuffer(), "Eleven");
08 
09    PrintBufferString(cChar10Buffer);
10    PrintBufferString(cChar11Buffer); // this will not compile
11 
12    return 0;
13}
Trying to call PrintBufferString() with cChar11Buffer will not work, because cChar11Buffer is a class of type Buffer<char, 11>, and PrintBufferString() only accepts classes of type Buffer<char, 10>. Even though Buffer<char, 10> and Buffer<char, 11> are both templated from the generic Buffer class, the different template parameters means they are treated as different classes, and can not be intermixed.
Although we could make a copy of PrintBufferString() that could handle Buffer<char, 11>, what happens when we want to call PrintBufferString() will a buffer of size 5, or 14? We’d have to copy the function for each different Buffer size we wanted to use.
Obviously full template specialization is too restrictive a solution here. The solution we are looking for is partial template specialization.
Partial template specialization
Partial template specialization allows us to write functions where some of the template parameters have been fully or partially resolved. In this case, the ideal solution would be to allow PrintBufferString() to accept char Buffers of any length. That means we have to specialize the templated data type, but leave the length in templated form. Fortunately, partial template specialization allows us to do just that!
1template<int nSize>
2void PrintBufferString(Buffer<char, nSize> &rcBuf)
3{
4    std::cout << rcBuf.GetBuffer() << std::endl;
5}
As you can see here, we’ve explicitly declared that this function will only work for Buffers of type char, but nSize is still a templated parameter, so it will work for char buffers of any size. That’s all there is to it!
Consider the following example:
01int main()
02{
03    // declare an integer buffer with room for 12 chars
04    Buffer<char, 10> cChar10Buffer;
05    Buffer<char, 11> cChar11Buffer;
06 
07    // strcpy a string into the buffer and print it
08    strcpy(cChar10Buffer.GetBuffer(), "Ten");
09    strcpy(cChar11Buffer.GetBuffer(), "Eleven");
10 
11    PrintBufferString(cChar10Buffer);
12    PrintBufferString(cChar11Buffer);
13 
14    return 0;
15}
This prints:
Ten
Eleven
Just as we expect.
Partial template specialization for pointers
In the previous lesson on expression parameters and template specialization, we took a look at a simple templated Storage class:
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};
We showed that this class had problems when template parameter T was of type char* because of the shallow copy/pointer assignment that takes place in the constructor. In that lesson, we used full template specialization to create a specialized version of the Storage constructor for type char* that allocated memory and created an actual deep copy of tValue. For reference, here’s the fully specialized char* Storage constructor:
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}
While that worked great for Storage<char*>, what about other pointer types? It’s fairly easy to see that if T is any pointer type, then we run into the problem of the constructor doing a pointer assignment instead of making an actual copy of the element being pointed to.
Because full template specialization forces us to fully resolve templated types, in order to fix this issue we’d have to define a new specialized constructor for each and every pointer type we wanted to use Storage with! This leads to lots of duplicate code, which as you well know by now is something we want to avoid as much as possible.
Fortunately, partial template specialization offers us a convenient solution. In this case, we’ll use class partial template specialization to define a special version of Storage that works for pointer values:
01using namespace std;
02 
03template <typename T>
04class Storage<T*> // this is specialization of Storage that works with pointer types
05{
06private:
07    T* m_tValue;
08public:
09    Storage(T* tValue) // for pointer type T
10    {
11         m_tValue = new T(*tValue);
12    }
13 
14    ~Storage()
15    {
16        delete m_tValue;
17    }
18 
19    void Print()
20    {
21        std::cout << *m_tValue << std::endl;
22    }
23};
And an example of this working:
01int main()
02{
03    // Declare a non-pointer Storage to show it works
04    Storage<int> cIntStorage(5);
05 
06    // Declare a pointer Storage to show it works
07    int x = 7;
08    Storage<int*> cIntPtrStorage(&x);
09 
10    // If cIntPtrStorage did a pointer assignment on x,
11    // then changing x will change cIntPtrStorage too
12    x = 9;
13    cIntPtrStorage.Print();
14 
15    return 0;
16}
This prints the value:
7
The fact that we got a 7 here shows that cIntPtrStorage used the pointer version of Storage, which allocated it’s own copy of the int. If cIntPtrStorage had used the non-pointer version of Storage, it would have done a pointer assignment — and when we changed the value of x, we would have changed cIntPtrStorage’s value too.
Using partial template class specialization to create separate pointer and non-pointer implementations of a class is extremely useful when you want a class to handle both differently, but in a way that’s completely transparent to the end-user.

No comments: