POOMA: A C++ Toolkit for High-Performance Parallel Scientific Computing | ||
---|---|---|
Prev | Chapter 2. Programming with Templates | Next |
Most POOMA users need only understand a subset of available constructs for template programming. These constructs include
reading template declarations and understanding template parameters, both of which are used in this book.
template instantiation, i.e., specifying a particular type by specifying values for template parameters.
nested type names, which are types specified within a class definition.
We discuss each of these below.
Templates generalize writing class declarations by permitting class declarations dependent on other types. For example, consider writing a class storing a pair of integers and a class storing a pair of doubles. See Example 2-1. Almost all of the code for the two definitions is the same. Both of these definitions define a class with a constructor and storing two values named left and right having the same type. Only the classes' names and its use of types differ.
Example 2-1. Classes Storing Pairs of Values
// Declare a class storing a pair of integers. struct pairOfInts { pairOfInts(const int& left, const int& right) : left_(left), right_(right) {} int left_; int right_; }; // Declare a class storing a pair of doubles. struct pairOfDoubles { pairOfDoubles(const double& left, const double& right) : left_(left), right_(right) {} double left_; double right_; };
Using templates, we can use a template parameter to represent their different uses of types and write one templated class definition. See Example 2-2. The templated class definition is a copy of the common portions of the two preceding definitions. Because the two definitions differ only in their use of the int and double types, we replace these concrete types with a template parameter T. We precede, not follow, the class definition with template <typename T>. The constructor's parameters' types are changed to T, as are the data members' types.
Example 2-2. Templated Class Storing Pairs of Values
// Declare a template class storing a pair of values // with the same type. template <typename T> //struct pair { pair(const T& left, const T& right) //
: left_(left), right_(right) {} T left_; //
T right_; }; // Use a class storing a pair of integers.
pair<int> pair1; // Use a class storing a pair of doubles; pair<double> pair2;
To use a template class definition, template arguments follow the class name surrounded by angle brackets (<>). For example, pair<int> instantiates the pair template class definition with T equal to int. That is, the compiler creates a definition for pair<int> by copying pair's template definition and substituting int for each occurrence of T. The copy omits the template parameter declaration template <typename T> at the beginning of its definition. The result is a definition exactly the same as pairOfInts.
As we mentioned above, template instantiation is analogous to function application. A template class is analogous to a function; it is a function from types and constants to classes. The analogy between compile-time and run-time programming constructs can be extended. Table 2-1 lists these correspondences. For example, at run time, values consist of things such as integers, floating point numbers, pointers, functions, and objects. Programs compute by operating on these values. The compile-time values include types, and compile-time operations use these types. For both run-time and compile-time programming, C++ defines default sets of values that all conforming compilers must support. For example, 3 and 6.022e+23 are run-time values that any C++ compiler must accept. It must also accept the int, bool, and int* types.
Table 2-1. Correspondences Between Run-Time and Compile-Time Constructs
programming construct | run time | compile time |
---|---|---|
values | integers, strings, objects, functions, … | types, … |
create a value to store multiple values | object creation | class definition |
values stored within a collection | data member, member function | nested type name, nested class, static member function, constant integral values |
placeholder for "any particular value" | variable, e.g., "any int" | template argument, e.g., "any type" |
packaging repeated operations | A function generalizes a particular operation applied to different values. The function parameters are placeholders for particular values. | A template class generalizes a particular class definition using different types. The template parameters are placeholders for particular values. |
application | Use a function by appending function arguments surrounded by parentheses. | Use a template class by appending template arguments surrounded by angle brackets (<>). |
The set of supported run-time and compile-time values can be extended. Run-time values can be extended by creating new objects. Although not part of the default set of values, these objects are treated and operated on as values. To extend the set of compile-time values, class definitions are written. For example, Example 2-1 declares two new types pairOfInts and pairOfDoubles. Although not part of the set of built-in types, these types can be used in the same way that any other types can be used, e.g., declaring variables.
Functions generalize similar run-time operations, while template class generalize similar class definitions. A function definition generalizes a repeated run-time operation. For example, consider repeatedly printing the largest of two numbers:
std::cout < < (3 > 4 ? 3 : 4) < < std::endl; std::cout < < (4 > -13 ? 4 : -13) < < std::endl; std::cout < < (23 > 4 ? 23 : 4) < < std::endl; std::cout < < (0 > 3 ? 0 : 3) < < std::endl;Each statement is exactly the same except for the repeated two values. Thus, we can generalize these statements writing a function:
void maxOut(int a, int b) { std::cout < < (a > b ? a : b) < < std::endl; }The function's body consists of the statement with variables substituted for the two particular values. Each parameter variable is a placeholder that, when used, holds one particular value among the set of possible integral values. The function must be named to permit its use, and declarations for its two parameters follow. Using the function simplifies the code:
maxOut(3, 4); maxOut(4, -13); maxOut(23, 4); maxOut(0, 3);To use a function, the function's name precedes parentheses surrounding specific values for its parameters, but the function's return type is omitted.
A template class definition generalizes repeated class definitions. If two class definitions differ only in a few types, template parameters can be substituted. Each parameter is a placeholder that, when used, holds one particular value, i.e., type, among the set of possible values. The class definition is named to permit its use, and declarations for its parameters precede it. The example found in the previous section illustrates this transformation. Compare the original, untemplated classes in Example 2-1 with the templated class in Example 2-2. Note the notation for the template class parameters. template <typename T> precedes the class definition. The keyword typename indicates the template parameter is a type. T is the template parameter's name. (We could have used any other identifier such as pairElementType or foo.) Note that using class is equivalent to using typename so template <class T> is equivalent to template <typename T>. While declaring a template class requires prefix notation, using a templated class requires postfix notation. The class's name precedes angle brackets (<>) surrounding specific values, i.e., types, for its parameters. As we showed above, pair<int> instantiates the template class pair with int for its type parameter T.
In template programming, nested type names store compile-time data that can be used within template classes. Since compile-time class definitions are analogous to run-time objects and the latter stores named values, nested type names are values, i.e., types, stored within class definitions. For example, the template class Array has an nested type name for the type of its domain:
typedef typename Engine_t::Domain_t Domain_t;This typedef, i.e., type definition, defines the type Domain_t as equivalent to Engine_t::Domain_t. The :: operator selects the Domain_t nested type from inside the Engine_t type. This illustrates how to access Array's Domain_t when not within Array's scope: Array<Dim, T, EngineTag>::Domain_t. The analogy between object members and nested type names alludes to its usefulness. Just as run-time object members store information for later use, nested type names store type information for later use at compile time. Using nested type names has no impact on the speed of executing programs.