Tuesday, January 18, 2011

Function template instances

Function template instances
It’s worth taking a brief look at how template functions are implemented in C++, because future lessons will build off of some of these concepts. It turns out that C++ does not compile the template function directly. Instead, at compile time, when the compiler encounters a call to a template function, it replicates the template function and replaces the template type parameters with actual types! The function with actual types is called a function template instance.
Let’s take a look at an example of this process. First, we have a templated function:
1template <typename Type> // this is the template parameter declaration
2Type max(Type tX, Type tY)
3{
4    return (tX > tY) ? tX : tY;
5}
When compiling your program, the compiler encounters a call to the templated function:
1int nValue = max(3, 7); // calls max(int, int)
The compiler says, “oh, we want to call max(int, int)”. The compiler replicates the function template and creates the template instance max(int, int):
1int max(int tX, int tY)
2{
3    return (tX > tY) ? tX : tY;
4}
This is now a “normal function” that can be compiled into machine language.
Now, let’s say later in your code, you called max() again using a different type:
1double dValue = max(6.34, 18.523); // calls max(double, double)
C++ automatically creates a template instance for max(double, double):
1double max(double tX, double tY)
2{
3    return (tX > tY) ? tX : tY;
4}
and then compiles it into machine language.
It’s worth noting that the compiler is smart enough to know it only needs to create one template instance per set of unique type parameters. It’s also worth noting that if you create a template function but do not call it, no template instances will be created.
Operators, function calls, and function templates
Template functions will work with both built-in types (eg. char, int, double, etc…) and classes, with one caveat. When the compiler compiles the template instance, it compiles it just like a normal function. In a normal function, any operators or function calls that you use with your types must be defined, or you will get a compiler error. Similarly, any operators or function calls in your template function must be defined for any types the function template is instantiated for. Let’s take a look at this in more detail.
First, we’ll create a simple class:
01class Cents
02{
03private:
04    int m_nCents;
05public:
06    Cents(int nCents)
07        : m_nCents(nCents)
08    {
09    }
10};
Now, let’s see what happens when we try to call our templated max() function with the Cents class:
1Cents cNickle(5);
2Cents cDime(10);
3 
4Cents cBigger = max(cNickle, cDime);
C++ will create a template instance for max() that looks like this:
1Cents max(Cents tX, Cents tY)
2{
3    return (tX > tY) ? tX : tY;
4}
And then it will try to compile this function. See the problem here? C++ has no idea how to evaluate tX > tY! Consequently, this will produce a compile error.
To get around this problem, simply overload the > operator for any class we wish to use max() with:
01class Cents
02{
03private:
04    int m_nCents;
05public:
06    Cents(int nCents)
07        : m_nCents(nCents)
08    {
09    }
10 
11    friend bool operator>(Cents &c1, Cents&c2)
12    {
13        return (c1.m_nCents > c2.m_nCents) ? true: false;
14    }
15};
Now C++ will know how to compare tX > tY when tX and tY are objects of the Cents class! As a result, our max() function will now work with two objects of type Cents.
Another example
Let’s do one more example of a function template. The following function template will calculate the average of a number of objects in an array:
01template <class T>
02T Average(T *atArray, int nNumValues)
03{
04    T tSum = 0;
05    for (int nCount=0; nCount < nNumValues; nCount++)
06        tSum += atArray[nCount];
07 
08    tSum /= nNumValues;
09    return tSum;
10}
Now let’s see it in action:
1int anArray[] = { 5, 3, 2, 1, 4 };
2cout << Average(anArray, 5) << endl;
3 
4double dnArray[] = { 3.12, 3.45, 9.23, 6.34 };
5cout << Average(dnArray, 4) << endl;
This produces the values:
3
5.535
As you can see, it works great for built-in types!
Now let’s see what happens when we call this function on our Cents class:
1Cents cArray[] = { Cents(5), Cents(10), Cents(15), Cents(14) };
2cout << Average(cArray, 4) << endl;
The compiler goes berserk and produces a ton of error messages! The first error message will be something like this:
c:\test.cpp(45) : error C2679: binary '<<' : no operator found which takes a right-hand operand of type 'Cents' (or there is no acceptable conversion)
Remember that Average() returns a Cents object, and we are trying to stream that object to cout using the << operator. However, we haven't defined the << operator for our Cents class yet. Let's do that:
01class Cents
02{
03private:
04    int m_nCents;
05public:
06    Cents(int nCents)
07        : m_nCents(nCents)
08    {
09    }
10 
11    friend ostream& operator<< (ostream &out, const Cents &cCents)
12    {
13        out << cCents.m_nCents << " cents ";
14        return out;
15    }
16};
If we compile again, we will get another error:
c:\test.cpp(14) : error C2676: binary '+=' : 'Cents' does not define this operator or a conversion to a type acceptable to the predefined operator
This error is actually being caused by the function template instance created when we call Average(Cents*, int). Remember that when we call a templated function, the compiler "stencils" out a copy of the function where the template type parameters (the placeholder types) have been replaced with the actual types in the function call. Here is the function template instance for Average() when T is a Cents object:
01template <class T>
02Cents Average(Cents  *atArray, int nNumValues)
03{
04    Cents tSum = 0;
05    for (int nCount=0; nCount < nNumValues; nCount++)
06        tSum += atArray[nCount];
07 
08    tSum /= nNumValues;
09    return tSum;
10}
The reason we are getting an error message is because of the following line:
1tSum += atArray[nCount];
In this case, tSum is a Cents object, but we have not defined the += operator for Cents objects! We will need to define this function in order for Average() to be able to work with Cents. Looking forward, we can see that Average() also uses the /= operator, so we will go ahead and define that as well:
01class Cents
02{
03private:
04    int m_nCents;
05public:
06    Cents(int nCents)
07        : m_nCents(nCents)
08    {
09    }
10 
11    friend ostream& operator<< (ostream &out, const Cents &cCents)
12    {
13        out << cCents.m_nCents << " cents ";
14        return out;
15    }
16 
17    void operator+=(Cents cCents)
18    {
19        m_nCents += cCents.m_nCents;
20    }
21 
22    void operator/=(int nValue)
23    {
24        m_nCents /= nValue;
25    }
26};
Finally, our code will compile and run! Here is the result:
11 cents
Note that we didn't have to modify Average() at all to make it work with objects of type Cents. We simply had to define the operators used to implement Average() for the Cents class, and the compiler took care of the rest!

No comments: