Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
1715
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Chapter 11

If you want to declare a function or method that takes a Grid object, you must specify the type stored in that grid as part of the Grid type:

void processIntGrid(Grid<int>& inGrid)

{

// Body omitted for brevity

}

The Grid template can store more than just ints. For example, you can instantiate a Grid that stores

SpreadsheetCells:

Grid<SpreadsheetCell> mySpreadsheet; SpreadsheetCell myCell; mySpreadsheet.setElementAt(3, 4, myCell);

You can store pointer types as well:

Grid<char*> myStringGrid;

myStringGrid.setElementAt(2, 2, “hello”);

The type specified can even be another template type. The following example uses the vector template from the standard template library (introduced in Chapter 4):

Grid<vector<int> > gridOfVectors; // Note the extra space! vector<int> myVector;

gridOfVectors.setElementAt(5, 6, myVector);

You must leave a space between the two closing angle brackets when you have nested templates. C++ requires this syntax because compilers would interpret >> in the following example as the I/O streams extraction operator:

Grid<vector<int>> gridOfVectors; // INCORRECT SYNTAX

You can also dynamically allocate Grid template instantiations on the heap:

Grid<int>* myGridp = new Grid<int>(); myGridp->setElementAt(0, 0, 10);

int x = myGridp->getElementAt(0, 0);

delete myGridp;

How the Compiler Processes Templates

In order to understand the intricacies of templates, you need to learn how the compiler processes template code. When the compiler encounters template method definitions, it performs syntax checking, but doesn’t actually compile the templates. It can’t compile template definitions because it doesn’t know for which types they will be used. It’s impossible for a compiler to generate code for something like x = y without knowing the types of x and y.

When the compiler encounters an instantiation of the template, such as Grid<int> myIntGrid, it writes code for an int version of the Grid template by replacing each T in the template class definition

280

Writing Generic Code with Templates

with int. When the compiler encounters a different instantiation of the template, such as

Grid<SpreadsheetCell> mySpreadsheet, it writes another version of the Grid class for

SpreadsheetCells. The compiler just writes the code that you would write if you didn’t have template support in the language and had to write separate classes for each element type. There’s no magic here; templates just automate an annoying process. If you don’t instantiate a class template for any types in your program, then the class method definitions are never compiled.

This instantiation process explains why you need to use the Grid<T> syntax in various places in your definition. When the compiler instantiates the template for a particular type, such as int, it replaces T with int, so that Grid<int> is the type.

Selective Instantiation

Instantiating a template for many different types can lead to code bloat because the compiler generates copies of the template code for each type. You can end up with large executable files when you use templates.

However, the problem is ameliorated because the compiler only generates code for the class methods that you actually call for a particular type. For example, given the Grid template class above, suppose that you write this code (and only this code) in main():

Grid<int> myIntGrid;

myIntGrid.setElementAt(0, 0, 10);

The compiler generates only the 0-argument constructor, the destructor, and the setElementAt() method for an int version of the Grid. It does not generate other methods like the copy constructor, the assignment operator, or getHeight().

Template Requirements on Types

When you write code that is independent of types, you must assume certain things about those types. For example, in the Grid template, you assume that the element type (represented by T) will have an assignment operator because of this line: mCells[x][y] = inElem. Similarly, you assume it will have a default constructor to allow you to create an array of elements.

If you attempt to instantiate a template with a type that does not support all the operations used by the template in your particular program, the code will not compile. However, even if the type you want to use doesn’t support the operations required by all the template code, you can exploit selective instantiation to use some methods but not others. For example, if you try to create a grid for an object that has no assignment operator, but you never call setElementAt() on that grid, your code will work fine. As soon as you try to call setElementAt(), however, you will receive a compilation error.

Distributing Template Code between Files

Normally you put class definitions in a header file and method definitions in a source file. Code that creates or uses objects of the class #includes the header file and obtains access to the method code via the linker. Templates don’t work that way. Because they are “templates” for the compiler to generate the actual methods for the instantiated types, both template class definitions and method definitions must be available to the compiler in any source file that uses them. In this sense, methods of a template class are similar to inline methods. There are several mechanisms to obtain this inclusion.

281

Chapter 11

Template Definitions in Header Files

You can place the method definitions directly in the same header file where you define the class itself. When you #include this file in a source file where you use the template, the compiler will have access to all the code it needs.

Alternatively, you can place the template method definitions in a separate header file that you #include in the header file with the class definitions. Make sure the #include for the method definitions follows the class definition; otherwise the code won’t compile!

// Grid.h

template <typename T> class Grid

{

// Class definition omitted for brevity

};

#include “GridDefinitions.h”

This division helps keep the distinction between class definitions and method definitions.

Template Definitions in Source Files

Method implementations look strange in header files. If that syntax annoys you, there is a way that you can place the method definitions in a source file. However, you still need to make the definitions available to the code that uses the templates, which you can do by #includeing the method implementation source file in the template class definition header file. That sounds odd if you’ve never seen it before, but it’s legal in C++. The header file looks like this:

// Grid.h

template <typename T> class Grid

{

// Class definition omitted for brevity

};

#include “Grid.cpp”

The C++ standard actually defines a way for template method definitions to exist in a source file, which does not need to be #included in a header file. You use the export keyword to specify that the template definitions should be available in all translation units (source files). Unfortunately, as of this writing, few commercial compilers support this feature, and many vendors seem disinclined to support it anytime soon.

Template Parameters

In the Grid example, the Grid template has one template parameter: the type that is stored in the grid. When you write the class template, you specify the parameter list inside the angle brackets, like this:

template <typename T>

282

Writing Generic Code with Templates

This parameter list is similar to the parameter list in a function or method. As in functions and methods, you can write a class with as many template parameters as you want. Additionally, these parameters don’t have to be types, and they can have default values.

Nontype Template Parameters

Nontype parameters are “normal” parameters such as ints and pointers: the kind of parameters with which you’re familiar from functions and methods. However, templates allow nontype parameters to be values of only “simple” types: ints, enums, pointers, and references.

In the Grid template class, you could use nontype template parameters to specify the height and width of the grid instead of specifying them in the constructor. The principle advantage to specifying nontype parameters in the template list instead of in the constructor is that the values are known before the code is compiled. Recall that the compiler generates code for templatized methods by substituting in the template parameters before compiling. Thus, you can use a normal two-dimensional array in your implementation instead of dynamically allocating it. Here is the new class definition:

template <typename T, int WIDTH, int HEIGHT> class Grid

{

public:

void setElementAt(int x, int y, const T& inElem); T& getElementAt(int x, int y);

const T& getElementAt(int x, int y) const; int getHeight() const { return HEIGHT; }

int getWidth() const { return WIDTH; }

protected:

T mCells[WIDTH][HEIGHT];

};

This class is significantly simpler than the old version. Note that the template parameter list requires three parameters: the type of object stored in the grid and the width and height of the grid. The width and height are used to create a two-dimensional array to store the objects. There is no dynamically allocated memory in the class, so it no longer needs a user-defined copy constructor, destructor, or assignment operator. In fact, you don’t even need to write a default constructor; the compiler generated one is just fine. Here are the class method definitions:

template <typename T, int WIDTH, int HEIGHT>

void Grid<T, WIDTH, HEIGHT>::setElementAt(int x, int y, const T& inElem)

{

mCells[x][y] = inElem;

}

template <typename T, int WIDTH, int HEIGHT>

T& Grid<T, WIDTH, HEIGHT>::getElementAt(int x, int y)

{

return (mCells[x][y]);

}

template <typename T, int WIDTH, int HEIGHT>

const T& Grid<T, WIDTH, HEIGHT>::getElementAt(int x, int y) const

{

return (mCells[x][y]);

}

283

Chapter 11

Note that wherever you previously specified Grid<T> you must now specify Grid<T, WIDTH, HEIGHT> to represent the three template parameters.

You can instantiate this template and use it like this:

Grid<int, 10, 10> myGrid;

Grid<int, 10, 10> anotherGrid;

myGrid.setElementAt(2, 3, 45); anotherGrid = myGrid;

cout << anotherGrid.getElementAt(2, 3);

This code seems great! Despite the slightly messy syntax for declaring a Grid, the actual Grid code is a lot simpler. Unfortunately, there are more restrictions than you might think at first. First, you can’t use a nonconstant integer to specify the height or width. The following code doesn’t compile:

int height = 10;

Grid<int, 10, height> testGrid; // DOES NOT COMPILE

However, if you make height const, it compiles:

const int height = 10;

Grid<int, 10, height> testGrid; // compiles and works

The second problem is much more significant. Now that the width and height are template parameters, they are part of the type of each grid. That means that Grid<int, 10, 10> and Grid<int, 10, 11> are two different types. You can’t assign an object of one type to an object of the other, and variables of one type can’t be passed to functions or methods that expect variables of another type.

Nontype template parameters become part of the type specification of instantiated objects.

Default Values for Integer Nontype Parameters

If you continue the approach of making height and width template parameters, you might want to be able to provide defaults for the height and width just as you did previously in the constructor of the Grid<T> class. C++ allows you to provide defaults for template parameters with a similar syntax. Here is the class definition:

template <typename T, int WIDTH = 10, int HEIGHT = 10> class Grid

{

// Remainder of the implementation is identical to the previous version

};

284