分类: C/C++
2012-05-09 10:05:18
In the previous two lessons, you learn how and could be used to generalize functions to work with many different data types. While this is a great start down the road to generalized programming, it doesn’t solve all of our problems. Let’s take a look at an example of one such problem, and see what templates can do for us further.
Templates and container classes
In the lesson on , you learned how to use composition to implement classes that contained multiple instances of other classes. As one example of such a container, we took a look at the IntArray class. Here is a simplified example of that class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #ifndef INTARRAY_H #define INTARRAY_H #include class IntArray { private: int m_nLength; int *m_pnData; public: IntArray() { m_nLength = 0; m_pnData = 0; } IntArray(int nLength) { m_pnData = new int[nLength]; m_nLength = nLength; } ~IntArray() { delete[] m_pnData; } void Erase() { delete[] m_pnData; // We need to make sure we set m_pnData to 0 here, otherwise it will // be left pointing at deallocated memory! m_pnData = 0; m_nLength = 0; } int& operator[](int nIndex) { assert(nIndex >= 0 && nIndex < m_nLength); return m_pnData[nIndex]; } int GetLength() { return m_nLength; } }; #endif |
While this class provides an easy way to create arrays of integers, what if we want to create an array of doubles? Using traditional programming methods, we’d have to create an entirely new class! Here’s an example of DoubleArray, an array class used to hold doubles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #ifndef DOUBLEARRAY_H #define DOUBLEARRAY_H #include class DoubleArray { private: int m_nLength; double *m_pdData; public: DoubleArray() { m_nLength = 0; m_pdData= 0; } DoubleArray(int nLength) { m_pdData= new double[nLength]; m_nLength = nLength; } ~DoubleArray() { delete[] m_pdData; } void Erase() { delete[] m_pdData; // We need to make sure we set m_pnData to 0 here, otherwise it will // be left pointing at deallocated memory! m_pdData= 0; m_nLength = 0; } double& operator[](int nIndex) { assert(nIndex >= 0 && nIndex < m_nLength); return m_pdData[nIndex]; } // The length of the array is always an integer // It does not depend on the data type of the array int GetLength() { return m_nLength; } }; #endif |
Although the code listings are lengthy, you’ll note the two classes are almost identical! In fact, the only substantive difference is the contained data type. As you likely have guessed, this is another area where templates can be put to good use to free us from having to create classes that are bound to one specific data type.
Creating template classes is works pretty much identically to creating template functions, so we’ll proceed by example. Here’s the IntArray classes, templatated version:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | #ifndef ARRAY_H #define ARRAY_H #include template class Array { private: int m_nLength; T *m_ptData; public: Array() { m_nLength = 0; m_ptData = 0; } Array(int nLength) { m_ptData= new T[nLength]; m_nLength = nLength; } ~Array() { delete[] m_ptData; } void Erase() { delete[] m_ptData; // We need to make sure we set m_pnData to 0 here, otherwise it will // be left pointing at deallocated memory! m_ptData= 0; m_nLength = 0; } T& operator[](int nIndex) { assert(nIndex >= 0 && nIndex < m_nLength); return m_ptData[nIndex]; } // The length of the array is always an integer // It does not depend on the data type of the array int GetLength(); // templated GetLength() function defined below }; template int Array #endif |
As you can see, this version is almost identical to the IntArray version, except we’ve added the template declaration, and changed the contained data type from int to T.
Note that we’ve also defined the GetLength() function outside of the class declaration. This isn’t necessary, but new programmers typically stumble when trying to do this for the first time due to the syntax, so an example is instructive. Each templated member function declared outside the class declaration needs its own template declaration. Also, note that the name of the templated array class is Array
Here’s a short example using the above templated array class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int main() { Array Array for (int nCount = 0; nCount < 12; nCount++) { anArray[nCount] = nCount; adArray[nCount] = nCount + 0.5; } for (int nCount = 11; nCount >= 0; nCount----;) std::cout << anArray[nCount] << "\t" << adArray[nCount] << std::endl; return 0; } |
This example prints the following:
11 11.5 10 10.5 9 9.5 8 8.5 7 7.5 6 6.5 5 5.5 4 4.5 3 3.5 2 2.5 1 1.5 0 0.5Templated classes are instanced in the same way templated functions are — the compile stencils a copy upon demand with the template parameter replaced by the actual data type the user needs and then compiles the copy. If you don’t ever use a template class, the compile won’t even compile it.
Template classes are ideal for implementing container classes, because it is highly desirable to have containers work across a wide variety of data types, and templates allow you to do so without duplicating code. Although the syntax is ugly, and the error messages can be cryptic, template classes are truly one of C++’s best and most useful features.
A note for users using older compilers
Some older compilers (eg. Visual Studio 6) have a bug where the definition of template class functions must be put in the same file as the template class is defined in. Thus, if the template class were defined in X.h, the function definitions would have to also go in X.h (not X.cpp). This issue should be fixed in most/all modern compilers.
2.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:
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | template class Buffer { private: // The expression parameter controls the size of the array T m_atBuffer[nSize]; public: T* GetBuffer() { return m_atBuffer; } T& operator[](int nIndex) { return m_atBuffer[nIndex]; } }; int main() { // declare an integer buffer with room for 12 chars Buffer // Fill it up in order, then print it backwards for (int nCount=0; nCount < 12; nCount++) cIntBuffer[nCount] = nCount; for (int nCount=11; nCount >= 0; nCount--) std::cout << cIntBuffer[nCount] << " "; std::cout << std::endl; // declare a char buffer with room for 31 chars Buffer // strcpy a string into the buffer and print it strcpy(cCharBuffer.GetBuffer(), "Hello there!"); std::cout << cCharBuffer.GetBuffer() << std::endl; return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using namespace std; template class Storage { private: T m_tValue; public: Storage(T tValue) { m_tValue = tValue; } ~Storage() { } void Print() { std::cout << m_tValue << std::endl;; } }; |
The above code will work fine for many data types:
1 2 3 4 5 6 7 8 9 10 | int main() { // Define some storage units Storage Storage // Print out some values nValue.Print(); dValue.Print(); } |
This prints:
5 6.7Now, 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:
1 2 3 4 | void Storage { std::cout << std::scientific << m_tValue << std::endl; } |
When the compiler goes to instantiate Storage
As a result, when we rerun the above program, it will print:
5 6.700000e+000Now 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*:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | int main() { using namespace std; // Dynamically allocate a temporary string char *strString = new char[40]; // Ask user for their name cout << "Enter your name: "; cin >> strString; // Store the name Storage // Delete the temporary string delete strString; // Print out our value strValue.Print(); // This will print garbage } |
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
1 2 3 4 | Storage { m_tValue = tValue; } |
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:
1 2 3 4 5 6 7 | Storage { // Allocate memory to hold the tValue string m_tValue = new char[strlen(tValue)+1]; // Copy the actual tValue string into the m_tValue memory we just allocated strcpy(m_tValue, tValue); } |
Now when we allocate a variable of type Storage
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
1 2 3 4 | Storage { delete[] m_tValue; } |
Now when variables of type ~Storage
3.Class template specialization
In the previous lesson on , we saw how it was possible to specialize member functions of a template class in order to provide different functionality for specific data types. As it turns out, it is not only possible to specialize member functions of a template class, it is also possible to specialize an entire class!
Consider the case where you want to design a class that stores 8 objects. Here’s a simplified class to do so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template class Storage8 { private: T m_tType[8]; public: void Set(int nIndex, const T &tType) { m_tType[nIndex] = tType; } const T& Get(int nIndex) { return m_tType[nIndex]; } }; |
Because this class is templated, it will work fine for any given type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int main() { // Define a Storage8 for integers Storage8 for (int nCount=0; nCount<8; nCount++) cIntStorage.Set(nCount, nCount); for (int nCount=0; nCount<8; nCount++) std::cout << cIntStorage.Get(nCount) << std::endl; // Define a Storage8 for bool Storage8 for (int nCount=0; nCount<8; nCount++) cBoolStorage.Set(nCount, nCount & 3); for (int nCount=0; nCount<8; nCount++) std::cout << (cBoolStorage.Get(nCount) ? "true" : "false") << std::endl; return 0; } |
This example prints:
0 1 2 3 4 5 6 7 false true true true false true true trueWhile this class is completely functional, it turns out that the implementation of Storage8
As it turns out, using some basic bit logic, it’s possible to compress all 8 bools into a single byte, eliminating the wasted space altogether. However, in order to do this, we’ll effectively need to essentially revamp the class, replacing the array of 8 bools with a variable that is a single byte in size. While we could create an entirely new class to do so, this has one major downside: we have to give it a different name. Then the programmer has to remember that Storage8
Class template specialization
Class template specialization allows us to specialize a template class for a particular data type (or set of data types, if there are multiple templated parameters). In this case, we’re going to use class template specialization to write a customized version of Storage8
Class template specializations are treated as completely independent classes, even though they are allocated in the same way as the templated class. This means that we can change anything and everything about our specialization class, including the way it’s implemented and even the functions it makes public, just as if it were an independent class. Here’s our specialized class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | template <> // the following is a template class with no templated parameters class Storage8 { // What follows is just standard class implementation details private: unsigned char m_tType; public: void Set(int nIndex, bool tType) { // Figure out which bit we're setting/unsetting // This will put a 1 in the bit we're interested in turning on/off unsigned char nMask = 1 << nIndex; if (tType) // If we're setting a bit m_tType |= nMask; // Use bitwise-or to turn that bit on else // if we're turning a bit off m_tType &= ~nMask; // bitwise-and the inverse mask to turn that bit off } bool Get(int nIndex) { // Figure out which bit we're getting unsigned char nMask = 1 << nIndex; // bitwise-and to get the value of the bit we're interested in // Then implicit cast to boolean return m_tType & nMask; } }; |
First, note that we start off with template<>. The template keyword tells the compiler that what follows is templated, and the empty angle braces means that there aren’t any template parameters. In this case, there aren’t any template parameters because we’re replacing the only template parameter (typename T) with a specific type (bool).
Next, we add
All of the other changes are just class implementation details. You do not need to understand how the bit-logic works in order to use the class (though here’s a link to the lesson on if you want to figure it out, but need a refresher on how bitwise operators work).
Note that this specialization class utilizes a single unsigned char (1 byte) instead of an array of 8 bools (8 bytes).
Now, when we declare a class of type Storage8
We can use the exact same example as before to show both Storage8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int main() { // Define a Storage8 for integers (instantiates Storage8 Storage8 for (int nCount=0; nCount<8; nCount++) cIntStorage[nCount] = nCount; for (int nCount=0; nCount<8; nCount++) std::cout << cIntStorage[nCount] << std::endl; // Define a Storage8 for bool (instantiates Storage8 Storage8 for (int nCount=0; nCount<8; nCount++) cBoolStorage.Set(nCount, nCount & 3); for (int nCount=0; nCount<8; nCount++) std::cout << (cBoolStorage.Get(nCount) ? "true" : "false") << std::endl; return 0; } |
As you might expect, this prints the same result as the previous example that used the non-specialized version of Storage8
It’s worth noting again that keeping the public interface between your template class and all of the specializations identical is generally a good idea, as it makes them easier to use — however, it’s not strictly necessary.
4.Partial template specialization
In the lesson on , 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | template class Buffer { private: // The expression parameter controls the side of the array T m_atBuffer[nSize]; public: T* GetBuffer() { return m_atBuffer; } T& operator[](int nIndex) { return m_atBuffer[nIndex]; } }; int main() { // declare a char buffer Buffer // copy a value into the buffer strcpy(cChar10Buffer.GetBuffer(), "Ten"); return 0; } |
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 2 3 4 5 | template void PrintBufferString(Buffer { std::cout << rcBuf.GetBuffer() << std::endl; } |
This would allow us to do the following:
1 2 3 4 5 6 7 8 9 10 11 12 | int main() { // declare a char buffer Buffer // copy a value into the buffer strcpy(cChar10Buffer.GetBuffer(), "Ten"); // Print the value PrintBufferString(cChar10Buffer); return 0; } |
and get the following result:
TenAlthough this works, it has a design flaw. Consider the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | int main() { // declare an int buffer Buffer // copy values into the buffer for (int nCount=0; nCount < 10; nCount++) cInt10Buffer[nCount] = nCount; // Print the value? PrintBufferString(cInt10Buffer); // what does this mean? return 0; } |
This program will compile, execute, and produce the following value (or one similar):
0012FF10What 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void PrintBufferString(Buffer { std::cout << rcBuf.GetBuffer() << std::endl; } int main() { // declare a char buffer Buffer // copy a value into the buffer strcpy(cChar10Buffer.GetBuffer(), "Ten"); // Print the value PrintBufferString(cChar10Buffer); return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | int main() { Buffer Buffer strcpy(cChar10Buffer.GetBuffer(), "Ten"); strcpy(cChar11Buffer.GetBuffer(), "Eleven"); PrintBufferString(cChar10Buffer); PrintBufferString(cChar11Buffer); // this will not compile return 0; } |
Trying to call PrintBufferString() with cChar11Buffer will not work, because cChar11Buffer is a class of type Buffer
Although we could make a copy of PrintBufferString() that could handle Buffer
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!
1 2 3 4 5 | template void PrintBufferString(Buffer { 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int main() { // declare an integer buffer with room for 12 chars Buffer Buffer // strcpy a string into the buffer and print it strcpy(cChar10Buffer.GetBuffer(), "Ten"); strcpy(cChar11Buffer.GetBuffer(), "Eleven"); PrintBufferString(cChar10Buffer); PrintBufferString(cChar11Buffer); return 0; } |
This prints:
Ten ElevenJust as we expect.
Partial template specialization for pointers
In the previous lesson on , we took a look at a simple templated Storage class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using namespace std; template class Storage { private: T m_tValue; public: Storage(T tValue) { m_tValue = tValue; } ~Storage() { } void Print() { 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 2 3 4 5 6 7 | Storage { // Allocate memory to hold the tValue string m_tValue = new char[strlen(tValue)+1]; // Copy the actual tValue string into the m_tValue memory we just allocated strcpy(m_tValue, tValue); } |
While that worked great for Storage
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using namespace std; template class Storage { private: T* m_tValue; public: Storage(T* tValue) // for pointer type T { m_tValue = new T(*tValue); } ~Storage() { delete m_tValue; } void Print() { std::cout << *m_tValue << std::endl; } }; |
And an example of this working:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int main() { // Declare a non-pointer Storage to show it works Storage // Declare a pointer Storage to show it works int x = 7; Storage // If cIntPtrStorage did a pointer assignment on x, // then changing x will change cIntPtrStorage too x = 9; cIntPtrStorage.Print(); return 0; } |
This prints the value:
7The 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.
from:
class template:
Expression parameters and template specialization:
partial specialization:
partial specialization: