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:
01 | template < typename T, int nSize> |
09 | T* GetBuffer() { return m_atBuffer; } |
11 | T& operator[]( int nIndex) |
13 | return m_atBuffer[nIndex]; |
20 | Buffer< char , 10> cChar10Buffer; |
23 | strcpy (cChar10Buffer.GetBuffer(), "Ten" ); |
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:
1 | template < typename T, int nSize> |
2 | void PrintBufferString(Buffer<T, nSize> &rcBuf) |
4 | std::cout << rcBuf.GetBuffer() << std::endl; |
This would allow us to do the following:
04 | Buffer< char , 10> cChar10Buffer; |
07 | strcpy (cChar10Buffer.GetBuffer(), "Ten" ); |
10 | PrintBufferString(cChar10Buffer); |
and get the following result:
Ten
Although this works, it has a design flaw. Consider the following:
04 | Buffer< int , 10> cInt10Buffer; |
07 | for ( int nCount=0; nCount < 10; nCount++) |
08 | cInt10Buffer[nCount] = nCount; |
11 | PrintBufferString(cInt10Buffer); |
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:
01 | void PrintBufferString(Buffer< char , 10> &rcBuf) |
03 | std::cout << rcBuf.GetBuffer() << std::endl; |
09 | Buffer< char , 10> cChar10Buffer; |
12 | strcpy (cChar10Buffer.GetBuffer(), "Ten" ); |
15 | PrintBufferString(cChar10Buffer); |
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:
03 | Buffer< char , 10> cChar10Buffer; |
04 | Buffer< char , 11> cChar11Buffer; |
06 | strcpy (cChar10Buffer.GetBuffer(), "Ten" ); |
07 | strcpy (cChar11Buffer.GetBuffer(), "Eleven" ); |
09 | PrintBufferString(cChar10Buffer); |
10 | PrintBufferString(cChar11Buffer); |
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!
2 | void PrintBufferString(Buffer< char , nSize> &rcBuf) |
4 | std::cout << rcBuf.GetBuffer() << std::endl; |
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:
04 | Buffer< char , 10> cChar10Buffer; |
05 | Buffer< char , 11> cChar11Buffer; |
08 | strcpy (cChar10Buffer.GetBuffer(), "Ten" ); |
09 | strcpy (cChar11Buffer.GetBuffer(), "Eleven" ); |
11 | PrintBufferString(cChar10Buffer); |
12 | PrintBufferString(cChar11Buffer); |
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:
20 | std::cout << m_tValue << std::endl;; |
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:
1 | Storage< char *>::Storage( char * tValue) |
4 | m_tValue = new char [ strlen (tValue)+1]; |
6 | strcpy (m_tValue, tValue); |
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:
11 | m_tValue = new T(*tValue); |
21 | std::cout << *m_tValue << std::endl; |
And an example of this working:
04 | Storage< int > cIntStorage(5); |
08 | Storage< int *> cIntPtrStorage(&x); |
13 | cIntPtrStorage.Print(); |
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:
Post a Comment