- •Contents
- •Introduction
- •Who This Book Is For
- •What This Book Covers
- •How This Book Is Structured
- •What You Need to Use This Book
- •Conventions
- •Source Code
- •Errata
- •p2p.wrox.com
- •The Basics of C++
- •The Obligatory Hello, World
- •Namespaces
- •Variables
- •Operators
- •Types
- •Conditionals
- •Loops
- •Arrays
- •Functions
- •Those Are the Basics
- •Diving Deeper into C++
- •Pointers and Dynamic Memory
- •Strings in C++
- •References
- •Exceptions
- •The Many Uses of const
- •C++ as an Object-Oriented Language
- •Declaring a Class
- •Your First Useful C++ Program
- •An Employee Records System
- •The Employee Class
- •The Database Class
- •The User Interface
- •Evaluating the Program
- •What Is Programming Design?
- •The Importance of Programming Design
- •Two Rules for C++ Design
- •Abstraction
- •Reuse
- •Designing a Chess Program
- •Requirements
- •Design Steps
- •An Object-Oriented View of the World
- •Am I Thinking Procedurally?
- •The Object-Oriented Philosophy
- •Living in a World of Objects
- •Object Relationships
- •Abstraction
- •Reusing Code
- •A Note on Terminology
- •Deciding Whether or Not to Reuse Code
- •Strategies for Reusing Code
- •Bundling Third-Party Applications
- •Open-Source Libraries
- •The C++ Standard Library
- •Designing with Patterns and Techniques
- •Design Techniques
- •Design Patterns
- •The Reuse Philosophy
- •How to Design Reusable Code
- •Use Abstraction
- •Structure Your Code for Optimal Reuse
- •Design Usable Interfaces
- •Reconciling Generality and Ease of Use
- •The Need for Process
- •Software Life-Cycle Models
- •The Stagewise and Waterfall Models
- •The Spiral Method
- •The Rational Unified Process
- •Software-Engineering Methodologies
- •Extreme Programming (XP)
- •Software Triage
- •Be Open to New Ideas
- •Bring New Ideas to the Table
- •Thinking Ahead
- •Keeping It Clear
- •Elements of Good Style
- •Documenting Your Code
- •Reasons to Write Comments
- •Commenting Styles
- •Comments in This Book
- •Decomposition
- •Decomposition through Refactoring
- •Decomposition by Design
- •Decomposition in This Book
- •Naming
- •Choosing a Good Name
- •Naming Conventions
- •Using Language Features with Style
- •Use Constants
- •Take Advantage of const Variables
- •Use References Instead of Pointers
- •Use Custom Exceptions
- •Formatting
- •The Curly Brace Alignment Debate
- •Coming to Blows over Spaces and Parentheses
- •Spaces and Tabs
- •Stylistic Challenges
- •Introducing the Spreadsheet Example
- •Writing Classes
- •Class Definitions
- •Defining Methods
- •Using Objects
- •Object Life Cycles
- •Object Creation
- •Object Destruction
- •Assigning to Objects
- •Distinguishing Copying from Assignment
- •The Spreadsheet Class
- •Freeing Memory with Destructors
- •Handling Copying and Assignment
- •Different Kinds of Data Members
- •Static Data Members
- •Const Data Members
- •Reference Data Members
- •Const Reference Data Members
- •More about Methods
- •Static Methods
- •Const Methods
- •Method Overloading
- •Default Parameters
- •Inline Methods
- •Nested Classes
- •Friends
- •Operator Overloading
- •Implementing Addition
- •Overloading Arithmetic Operators
- •Overloading Comparison Operators
- •Building Types with Operator Overloading
- •Pointers to Methods and Members
- •Building Abstract Classes
- •Using Interface and Implementation Classes
- •Building Classes with Inheritance
- •Extending Classes
- •Overriding Methods
- •Inheritance for Reuse
- •The WeatherPrediction Class
- •Adding Functionality in a Subclass
- •Replacing Functionality in a Subclass
- •Respect Your Parents
- •Parent Constructors
- •Parent Destructors
- •Referring to Parent Data
- •Casting Up and Down
- •Inheritance for Polymorphism
- •Return of the Spreadsheet
- •Designing the Polymorphic Spreadsheet Cell
- •The Spreadsheet Cell Base Class
- •The Individual Subclasses
- •Leveraging Polymorphism
- •Future Considerations
- •Multiple Inheritance
- •Inheriting from Multiple Classes
- •Naming Collisions and Ambiguous Base Classes
- •Interesting and Obscure Inheritance Issues
- •Special Cases in Overriding Methods
- •Copy Constructors and the Equals Operator
- •The Truth about Virtual
- •Runtime Type Facilities
- •Non-Public Inheritance
- •Virtual Base Classes
- •Class Templates
- •Writing a Class Template
- •How the Compiler Processes Templates
- •Distributing Template Code between Files
- •Template Parameters
- •Method Templates
- •Template Class Specialization
- •Subclassing Template Classes
- •Inheritance versus Specialization
- •Function Templates
- •Function Template Specialization
- •Function Template Overloading
- •Friend Function Templates of Class Templates
- •Advanced Templates
- •More about Template Parameters
- •Template Class Partial Specialization
- •Emulating Function Partial Specialization with Overloading
- •Template Recursion
- •References
- •Reference Variables
- •Reference Data Members
- •Reference Parameters
- •Reference Return Values
- •Deciding between References and Pointers
- •Keyword Confusion
- •The const Keyword
- •The static Keyword
- •Order of Initialization of Nonlocal Variables
- •Types and Casts
- •typedefs
- •Casts
- •Scope Resolution
- •Header Files
- •C Utilities
- •Variable-Length Argument Lists
- •Preprocessor Macros
- •How to Picture Memory
- •Allocation and Deallocation
- •Arrays
- •Working with Pointers
- •Array-Pointer Duality
- •Arrays Are Pointers!
- •Not All Pointers Are Arrays!
- •Dynamic Strings
- •C-Style Strings
- •String Literals
- •The C++ string Class
- •Pointer Arithmetic
- •Custom Memory Management
- •Garbage Collection
- •Object Pools
- •Function Pointers
- •Underallocating Strings
- •Memory Leaks
- •Double-Deleting and Invalid Pointers
- •Accessing Out-of-Bounds Memory
- •Using Streams
- •What Is a Stream, Anyway?
- •Stream Sources and Destinations
- •Output with Streams
- •Input with Streams
- •Input and Output with Objects
- •String Streams
- •File Streams
- •Jumping around with seek() and tell()
- •Linking Streams Together
- •Bidirectional I/O
- •Internationalization
- •Wide Characters
- •Non-Western Character Sets
- •Locales and Facets
- •Errors and Exceptions
- •What Are Exceptions, Anyway?
- •Why Exceptions in C++ Are a Good Thing
- •Why Exceptions in C++ Are a Bad Thing
- •Our Recommendation
- •Exception Mechanics
- •Throwing and Catching Exceptions
- •Exception Types
- •Throwing and Catching Multiple Exceptions
- •Uncaught Exceptions
- •Throw Lists
- •Exceptions and Polymorphism
- •The Standard Exception Hierarchy
- •Catching Exceptions in a Class Hierarchy
- •Writing Your Own Exception Classes
- •Stack Unwinding and Cleanup
- •Catch, Cleanup, and Rethrow
- •Use Smart Pointers
- •Common Error-Handling Issues
- •Memory Allocation Errors
- •Errors in Constructors
- •Errors in Destructors
- •Putting It All Together
- •Why Overload Operators?
- •Limitations to Operator Overloading
- •Choices in Operator Overloading
- •Summary of Overloadable Operators
- •Overloading the Arithmetic Operators
- •Overloading Unary Minus and Unary Plus
- •Overloading Increment and Decrement
- •Overloading the Subscripting Operator
- •Providing Read-Only Access with operator[]
- •Non-Integral Array Indices
- •Overloading the Function Call Operator
- •Overloading the Dereferencing Operators
- •Implementing operator*
- •Implementing operator->
- •What in the World Is operator->* ?
- •Writing Conversion Operators
- •Ambiguity Problems with Conversion Operators
- •Conversions for Boolean Expressions
- •How new and delete Really Work
- •Overloading operator new and operator delete
- •Overloading operator new and operator delete with Extra Parameters
- •Two Approaches to Efficiency
- •Two Kinds of Programs
- •Is C++ an Inefficient Language?
- •Language-Level Efficiency
- •Handle Objects Efficiently
- •Use Inline Methods and Functions
- •Design-Level Efficiency
- •Cache as Much as Possible
- •Use Object Pools
- •Use Thread Pools
- •Profiling
- •Profiling Example with gprof
- •Cross-Platform Development
- •Architecture Issues
- •Implementation Issues
- •Platform-Specific Features
- •Cross-Language Development
- •Mixing C and C++
- •Shifting Paradigms
- •Linking with C Code
- •Mixing Java and C++ with JNI
- •Mixing C++ with Perl and Shell Scripts
- •Mixing C++ with Assembly Code
- •Quality Control
- •Whose Responsibility Is Testing?
- •The Life Cycle of a Bug
- •Bug-Tracking Tools
- •Unit Testing
- •Approaches to Unit Testing
- •The Unit Testing Process
- •Unit Testing in Action
- •Higher-Level Testing
- •Integration Tests
- •System Tests
- •Regression Tests
- •Tips for Successful Testing
- •The Fundamental Law of Debugging
- •Bug Taxonomies
- •Avoiding Bugs
- •Planning for Bugs
- •Error Logging
- •Debug Traces
- •Asserts
- •Debugging Techniques
- •Reproducing Bugs
- •Debugging Reproducible Bugs
- •Debugging Nonreproducible Bugs
- •Debugging Memory Problems
- •Debugging Multithreaded Programs
- •Debugging Example: Article Citations
- •Lessons from the ArticleCitations Example
- •Requirements on Elements
- •Exceptions and Error Checking
- •Iterators
- •Sequential Containers
- •Vector
- •The vector<bool> Specialization
- •deque
- •list
- •Container Adapters
- •queue
- •priority_queue
- •stack
- •Associative Containers
- •The pair Utility Class
- •multimap
- •multiset
- •Other Containers
- •Arrays as STL Containers
- •Strings as STL Containers
- •Streams as STL Containers
- •bitset
- •The find() and find_if() Algorithms
- •The accumulate() Algorithms
- •Function Objects
- •Arithmetic Function Objects
- •Comparison Function Objects
- •Logical Function Objects
- •Function Object Adapters
- •Writing Your Own Function Objects
- •Algorithm Details
- •Utility Algorithms
- •Nonmodifying Algorithms
- •Modifying Algorithms
- •Sorting Algorithms
- •Set Algorithms
- •The Voter Registration Audit Problem Statement
- •The auditVoterRolls() Function
- •The getDuplicates() Function
- •The RemoveNames Functor
- •The NameInList Functor
- •Testing the auditVoterRolls() Function
- •Allocators
- •Iterator Adapters
- •Reverse Iterators
- •Stream Iterators
- •Insert Iterators
- •Extending the STL
- •Why Extend the STL?
- •Writing an STL Algorithm
- •Writing an STL Container
- •The Appeal of Distributed Computing
- •Distribution for Scalability
- •Distribution for Reliability
- •Distribution for Centrality
- •Distributed Content
- •Distributed versus Networked
- •Distributed Objects
- •Serialization and Marshalling
- •Remote Procedure Calls
- •CORBA
- •Interface Definition Language
- •Implementing the Class
- •Using the Objects
- •A Crash Course in XML
- •XML as a Distributed Object Technology
- •Generating and Parsing XML in C++
- •XML Validation
- •Building a Distributed Object with XML
- •SOAP (Simple Object Access Protocol)
- •. . . Write a Class
- •. . . Subclass an Existing Class
- •. . . Throw and Catch Exceptions
- •. . . Read from a File
- •. . . Write to a File
- •. . . Write a Template Class
- •There Must Be a Better Way
- •Smart Pointers with Reference Counting
- •Double Dispatch
- •Mix-In Classes
- •Object-Oriented Frameworks
- •Working with Frameworks
- •The Model-View-Controller Paradigm
- •The Singleton Pattern
- •Example: A Logging Mechanism
- •Implementation of a Singleton
- •Using a Singleton
- •Example: A Car Factory Simulation
- •Implementation of a Factory
- •Using a Factory
- •Other Uses of Factories
- •The Proxy Pattern
- •Example: Hiding Network Connectivity Issues
- •Implementation of a Proxy
- •Using a Proxy
- •The Adapter Pattern
- •Example: Adapting an XML Library
- •Implementation of an Adapter
- •Using an Adapter
- •The Decorator Pattern
- •Example: Defining Styles in Web Pages
- •Implementation of a Decorator
- •Using a Decorator
- •The Chain of Responsibility Pattern
- •Example: Event Handling
- •Implementation of a Chain of Responsibility
- •Using a Chain of Responsibility
- •Example: Event Handling
- •Implementation of an Observer
- •Using an Observer
- •Chapter 1: A Crash Course in C++
- •Chapter 3: Designing with Objects
- •Chapter 4: Designing with Libraries and Patterns
- •Chapter 5: Designing for Reuse
- •Chapter 7: Coding with Style
- •Chapters 8 and 9: Classes and Objects
- •Chapter 11: Writing Generic Code with Templates
- •Chapter 14: Demystifying C++ I/O
- •Chapter 15: Handling Errors
- •Chapter 16: Overloading C++ Operators
- •Chapter 17: Writing Efficient C++
- •Chapter 19: Becoming Adept at Testing
- •Chapter 20: Conquering Debugging
- •Chapter 24: Exploring Distributed Objects
- •Chapter 26: Applying Design Patterns
- •Beginning C++
- •General C++
- •I/O Streams
- •The C++ Standard Library
- •C++ Templates
- •Integrating C++ and Other Languages
- •Algorithms and Data Structures
- •Open-Source Software
- •Software-Engineering Methodology
- •Programming Style
- •Computer Architecture
- •Efficiency
- •Testing
- •Debugging
- •Distributed Objects
- •CORBA
- •XML and SOAP
- •Design Patterns
- •Index
Writing Generic Code with Templates
template <typename T> class Grid
{
public:
// Omitted for brevity
friend ostream& operator<< <T>(ostream& ostr, const Grid<T>& grid); // Omitted for brevity
};
This friend declaration is tricky: you’re saying that, for an instance of the template with type T, the T instantiation of operator<< is a friend. In other words, there is a one-to-one mapping of friends
between the class instantiations and the function instantiations. Note particularly the explicit template specification <T> on operator<< (the space after operator<< is optional). This syntax tells the compiler that operator<< is itself a template. Some compilers fail to support this syntax, but it’s legal C++, and works on most new compilers.
Advanced Templates
The first half of this chapter covered the most widely used features of class and function templates. If you are interested in only a basic knowledge of templates so that you can use the STL or perhaps write your own simple classes, you can stop here. However, if templates interest you and you want to uncover their full power, read the second half of this chapter to learn about some of the more obscure, but fascinating, details.
More about Template Parameters
There are actually three kinds of template parameters: type, nontype and template template (no, you’re not seeing double: that really is the name!) You’ve seen examples of type and nontype parameters above, but not template template parameters yet. There are also some tricky aspects to both template and nontype parameters that were not covered above.
More about Template Type Parameters
Type parameters to templates are the main purpose of templates. You can declare as many type parameters as you want. For example, you could add to the grid template a second type parameter specifying another templatized class container on which to build the grid. Recall from Chapter 4 that the standard template library defines several templatized container classes, including vector and deque. In your original grid class you might want to have an array of vectors or an array of deques instead of just an array of arrays. With another template type parameter, you can allow the user to specify whether she wants the underlying container to be a vector or a deque. Here is the class definition with the additional template parameter:
template <typename T, typename Container> class Grid
{
public:
Grid(int inWidth = kDefaultWidth, int inHeight = kDefaultHeight); Grid(const Grid<T, Container>& src);
~Grid();
299
Chapter 11
Grid<T, Container>& operator=(const Grid<T, Container>& rhs); 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 mHeight; } int getWidth() const { return mWidth; } static const int kDefaultWidth = 10; static const int kDefaultHeight = 10;
protected:
void copyFrom(const Grid<T, Container>& src);
Container* mCells; int mWidth, mHeight;
};
This template now has two parameters: T and Container. Thus, wherever you previously referred to Grid<T> you must refer to Grid<T, Container> to specify both template parameters. The only other change is that mCells is now a pointer to a dynamically allocated array of Containers instead of a pointer to a dynamically allocated two-dimensional array of T elements.
Here is the constructor definition. It assumes that the Container type has a resize() method. If you try to instantiate this template by specifying a type that has no resize() method, the compiler will generate an error, as described below.
template <typename T, typename Container>
Grid<T, Container>::Grid(int inWidth, int inHeight) : mWidth(inWidth), mHeight(inHeight)
{
// Dynamically allocate the array of mWidth containers mCells = new Container[mWidth];
for (int i = 0; i < mWidth; i++) {
// Resize each container so that it can hold mHeight elements. mCells[i].resize(mHeight);
}
}
Here is the destructor definition. There’s only one call to new in the constructor, so only one call to delete in the destructor.
template <typename T, typename Container> Grid<T, Container>::~Grid()
{
delete [] mCells;
}
The code in copyFrom() assumes that you can access elements in the container using array [] notation. Chapter 16 explains how to overload the [] operator to implement this feature in your own container classes, but for now, it’s enough to know that the vector and deque from the STL both support this syntax.
template <typename T, typename Container>
void Grid<T, Container>::copyFrom(const Grid<T, Container>& src)
{
int i, j;
mWidth = src.mWidth; mHeight = src.mHeight;
300
Writing Generic Code with Templates
mCells = new Container[mWidth]; for (i = 0; i < mWidth; i++) {
// Resize each element, as in the constructor. mCells[i].resize(mHeight);
}
for (i = 0; i < mWidth; i++) {
for (j = 0; j < mHeight; j++) { mCells[i][j] = src.mCells[i][j];
}
}
}
Here are the implementations of the remaining methods.
template <typename T, typename Container>
Grid<T, Container>::Grid(const Grid<T, Container>& src)
{
copyFrom(src);
}
template <typename T, typename Container>
Grid<T, Container>& Grid<T, Container>::operator=(const Grid<T, Container>& rhs)
{
//Check for self-assignment. if (this == &rhs) {
return (*this);
}
//Free the old memory. delete [] mCells;
//Copy the new memory. copyFrom(rhs);
return (*this);
}
template <typename T, typename Container>
void Grid<T, Container>::setElementAt(int x, int y, const T& inElem)
{
mCells[x][y] = inElem;
}
template <typename T, typename Container>
T& Grid<T, Container>::getElementAt(int x, int y)
{
return (mCells[x][y]);
}
template <typename T, typename Container>
const T& Grid<T, Container>::getElementAt(int x, int y) const
{
return (mCells[x][y]);
}
301
Chapter 11
Now you can instantiate and use grid objects like this:
Grid<int, vector<int> > myIntGrid;
Grid<int, deque<int> > myIntGrid2;
myIntGrid.setElementAt(3, 4, 5);
cout << myIntGrid.getElementAt(3, 4);
Grid<int, vector<int> > grid2(myIntGrid); grid2 = myIntGrid;
The use of the word Container for the parameter name doesn’t mean that the type really must be a container. You could try to instantiate the Grid class with an int instead:
Grid<int, int> test; // WILL NOT COMPILE
This line will not compile, but it might not give you the error you expect. It won’t complain that the second type argument is an int instead of a container. Instead it will tell you that left of ‘.resize’ must have class/struct/union type. That’s because the compiler attempts to generate a Grid class with int as the Container. Everything works fine until it tries to compile this line:
mCells[i].resize(mHeight);
At that point, the compiler realizes that mCells[i] is an int, so you can’t call the resize() method on it!
This approach may seem convoluted and useless to you. However, it arises in the standard template library. The stack, queue, and priority_queue class templates all take a template type parameter specifying the underlying container, which can be a vector, deque, or list.
Default Values for Template Type Parameters
You can give template parameters default values. For example, you might want to say that the default container for your Grid is a vector. The template class definition would look like this:
#include <vector> using std::vector;
template <typename T, typename Container = vector<T> > class Grid
{
public:
// Everything else is the same as before.
};
You can use the type T from the first template parameter as the argument to the vector template in the default value for the second template parameter. Note also that you must leave a space between the two closing angle brackets to avoid the parsing problem discussed earlier in the chapter.
C++ syntax requires that you do not repeat the default value in the template header line for method definitions.
302
Writing Generic Code with Templates
With this default parameter, clients can now instantiate a grid with or without specifying an underlying container:
Grid<int, vector<int> > myIntGrid;
Grid<int> myIntGrid2;
Introducing Template Template Parameters
There is one problem with the Container parameter in the previous section. When you instantiate the class template, you write something like this:
Grid<int, vector<int> > myIntGrid;
Note the repetition of the int type. You must specify that it’s the element type both of the Grid and of the vector. What if you wrote this instead?
Grid<int, vector<SpreadsheetCell> > myIntGrid;
That wouldn’t work very well! It would be nice to be able to write the following, so that you couldn’t make that mistake:
Grid<int, vector> myIntGrid;
The Grid class should be able to figure out that it wants a vector of ints. The compiler won’t allow you to pass that argument to a normal type parameter, though, because vector by itself is not a type, but a template.
If you want to take a template as a template parameter, you must use a special kind of parameter called a template template parameter. The syntax is crazy, and some compilers don’t yet support it. However, if you’re still interested, read on.
Specifying a template template parameter is sort of like specifying a function pointer parameter in a normal function. Function pointer types include the return type and parameter types of a function. Similarly, when you specify a template template parameter, the full specification of the template template parameter includes the parameters to that template.
Containers in the STL have a template parameter list that looks something like this:
template <typename E, typename Allocator = allocator<E> > class vector
{
// Vector definition
};
The E parameter is simply the element type. Don’t worry about the Allocator for now — it’s covered in Chapter 21.
303
Chapter 11
Given the above template specification, here is the template class definition for the Grid class that takes a container template as its second template parameter:
template <typename T, template <typename E, typename Allocator = allocator<E> >
class Container = vector > class Grid
{
public:
//Omitted code that is the same as before Container<T>* mCells;
//Omitted code that is the same as before
};
What is going on here? The first template parameter is the same as before: the element type T. The second template parameter is now a template itself for a container such as vector or deque. As you saw earlier, this “template type” must take two parameters: an element type E and an allocator Allocator. Note the repetition of the word class after the nested template parameter list. The name of this parameter in the Grid template is Container (as before). The default value is now vector, instead of vector<T>, because the Container is a template instead of an actual type.
The syntax rule for a template template parameter more generically is this:
template <other params, ..., template <TemplateTypeParams> class ParameterName,
other params, ...>
Now that you’ve suffered through the above syntax to declare the template, the rest is easy. Instead of using Container by itself in the code, you must specify Container<T> as the container type you use. For example, the constructor now looks like this (you don’t repeat the default template template parameter argument in the template specification for the method definition):
template <typename T, template <typename E, typename Allocator = allocator<E> >
class Container>
Grid<T, Container>::Grid(int inWidth, int inHeight) : mWidth(inWidth), mHeight(inHeight)
{
mCells = new Container<T>[mWidth]; for (int i = 0; i < mWidth; i++) { mCells[i].resize(mHeight);
}
}
After implementing all the methods, you can use the template like this:
Grid<int, vector> myGrid;
myGrid.setElementAt(1, 2, 3); myGrid.getElementAt(1,2); Grid<int, vector> myGrid2(myGrid);
If you haven’t skipped this section entirely, you’re surely thinking at this point that C++ deserves every criticism that’s ever been thrown at it. Try not to bog down in the syntax here, and keep the main concept in mind: you can pass templates as parameters to other templates.
304
Writing Generic Code with Templates
More about Nontype Template Parameters
You might want to allow the user to specify an empty(not in the literal sense) element that is used to initialize each cell in the grid. Here is a perfectly reasonable approach to implement this goal:
template <typename T, const T EMPTY> class Grid
{
public:
Grid(int inWidth = kDefaultWidth, int inHeight = kDefaultHeight); Grid(const Grid<T, EMPTY>& src);
~Grid();
Grid<T, EMPTY>& operator=(const Grid<T, EMPTY>& rhs);
// Omitted for brevity
protected:
void copyFrom(const Grid<T, EMPTY>& src); T** mCells;
int mWidth, mHeight;
};
This definition is legal. You can use the type T from the first parameter as the type for the second parameter and nontype parameters can be const just like function parameters. You can use this initial value for T to initialize each cell in the grid:
template <typename T, const T EMPTY>
Grid<T, EMPTY>::Grid(int inWidth, int inHeight) : mWidth(inWidth), mHeight(inHeight)
{
mCells = new T* [mWidth];
for (int i = 0; i < mWidth; i++) { mCells[i] = new T[mHeight];
for (int j = 0; j < mHeight; j++) { mCells[i][j] = EMPTY;
}
}
}
The other method definitions stay the same, except that you must add the second type parameter to the template lines, and all the instances of Grid<T> become Grid<T, EMPTY>. After making those changes, you can then instantiate an int Grid with an initial value for all the elements:
Grid<int, 0> myIntGrid;
Grid<int, 10> myIntGrid2;
The initial value can be any integer you want. However, suppose that you try to create a
SpreasheetCell Grid:
SpreadsheetCell emptyCell;
Grid<SpreadsheetCell, emptyCell> mySpreadsheet; // WILL NOT COMPILE
That line leads to a compiler error because you cannot pass objects as arguments to nontype parameters.
305
Chapter 11
Nontype parameters cannot be objects, or even doubles or floats. They are restricted only to ints, enums, pointers, and references.
This example illustrates one of the vagaries of template classes: they can work correctly on one type but fail to compile for another type.
Reference and Pointer Nontype Template Parameters
A more comprehensive way of allowing the user to specify an initial empty element for the grid uses a reference to a T as the nontype template parameter. Here is the new class definition:
template <typename T, const T& EMPTY> class Grid
{
//Everything else is the same as the previous example, except the
//template lines in the method definitions specify const T& EMPTY
//instead of const T EMPTY.
};
Now you can instantiate this template class for any type. However, the reference you pass as the second template argument must refer to a global variable with external linkage. External linkage can be thought of as the opposite of static linkage, and just means that the variable is available in source files outside the one in which it is defined. See Chapter 12 for more details. For now, it suffices to know that you can declare that a variable has external linkage with the extern keyword:
extern const int x = 0;
Note that this line occurs outside of any function or method body. Here is a full program that declares int and SpreadsheetCell grids with initialization parameters:
#include “GridRefNonType.h” #include “SpreadsheetCell.h”
extern const int emptyInt = 0;
extern const SpreadsheetCell emptyCell(0);
int main(int argc, char** argv)
{
Grid<int, emptyInt> myIntGrid; Grid<SpreadsheetCell, emptyCell> mySpreadsheet;
Grid<int, emptyInt> myIntGrid2(myIntGrid);
return (0);
}
Reference and pointer template arguments must refer to global variables that are available from all translation units. The technical term for these types of variables is data with external linkage.
306