Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-2

.pdf
Скачиваний:
75
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

 

More on Classes

 

 

m_Free = 1;

// Next is free

}

 

// Default constructor

 

CSamples(){ m_Free = 0 }

// Nothing stored, so first is free

// Function to add a sample

 

bool Add(const T& value)

 

{

 

bool OK = m_Free < 100;

// Indicates there is a free place

if(OK)

 

m_Values[m_Free++] = value;

// OK true, so store the value

return OK;

 

}

 

// Function to obtain maximum sample T Max() const

{

// Set first sample or 0 as maximum T theMax = m_Free ? m_Values[0] : 0;

for(int i = 1; i < m_Free; i++) // Check all the samples if(m_Values[i] > theMax)

theMax = m_Values[i]; // Store any larger sample return theMax;

}

 

private:

 

T m_Values[100];

// Array to store samples

int m_Free;

// Index of free location in m_Values

};

To indicate that you are defining a template rather than a straightforward class, you insert the template keyword and the type parameter, T, between angled brackets, just before the keyword class and the class name, CSamples. This is essentially the same syntax that you used to define a function template in Chapter 6. The parameter T is the type variable that is replaced by a specific type when you declare a class object. Wherever the parameter T appears in the class definition, it is replaced by the type that you specify in your object declaration; this creates a class definition corresponding to this type. You can specify any type (a basic data type or a class type), but it has to make sense in the context of the class template, of course. Any class type that you use to instantiate a class from a template must have all the operators defined that the member functions of the template will use with such objects. If your class has- n’t implemented operator>(), for example, it does not work with the CSamples class template. In general, you can specify multiple parameters in a class template if you need them. I’ll come back to this possibility a little later in the chapter.

Getting back to the example, the type of the array in which the samples are stored is specified as T. The array will therefore be an array of whatever type you specify for T when you declare a CSamples object. As you can see, you also use the type T in two of the constructors for the class, as well as in the Add() and Max() functions. Each of these occurrences is also replaced when you instantiate a class object using the template.

429

Chapter 8

The constructors support the creation of an empty object, an object with a single sample, and an object initialized with an array of samples. The Add() function allows samples to be added to an object one at a time. You could also overload this function to add an array of samples. The class template includes some elementary provision to prevent the capacity of the m_Values array being exceeded in the Add() function, and in the constructor that accepts an array of samples.

As I said earlier, in theory you can create objects of CSamples classes that handle any data type: type int, type double, or any class type that you’ve defined. In practice, this doesn’t mean it necessarily compiles and works as you expect. It all depends on what the template definition does, and usually a template only works for a particular range of types. For example, the Max() function implicitly assumes that the > operator is available for whatever type is being processed. If it isn’t, your program does not compile. Clearly, you’ll usually be in the position of defining a template that works for some types but not others, but there’s no way you can restrict what type is applied to a template.

Template Member Functions

You may want to place the definition of a class template member function outside of the template definition. The syntax for this isn’t particularly obvious, so let’s look at how you do it. You put the function declaration in the class template definition in the normal way. For instance:

template <class T> class CSamples

{

// Rest of the template definition...

T Max()

const;

// Function to obtain maximum sample

// Rest

of the

template definition...

}

This declares the Max() function as a member of the class template but doesn’t define it. You now need to create a separate function template for the definition of the member function. You must use the template class name plus the parameters in angled brackets to identify the class template to which the function template belongs:

template<class T>

 

T CSamples<T>::Max() const

 

{

 

T theMax = m_Values[0];

// Set first sample as maximum

for(int i = 1; i < m_Free; i++)

// Check all the samples

if(m_Values[i] > theMax)

 

theMax = m_Values[i];

// Store any larger sample

return theMax;

 

}

 

 

 

You saw the syntax for a function template in Chapter 6. Because this function template is for a member of the class template with the parameter T, the function template definition here should have the same parameters as the class template definition. There’s just one in this case — T — but in general there can be several. If the class template had two or more parameters, so would each template defining a member function.

430

More on Classes

Note how you only put the parameter name T along with the class name before the scope resolution operator. This is necessary — the parameters are fundamental to the identification of the class to which a function, produced from the template, belongs. The type is CSamples<T> with whatever type you assign to T when you create an instance of the class template. Your type is plugged into the class template to generate the class definition and into the function template to generate the definition for the Max() function for the class. Each class produced from the class template needs to have its own definition for the function Max().

Defining a constructor or a destructor outside of the class template definition is similar. You could write the definition of the constructor that accepts an array of samples as:

template<class T>

CSamples<T>::CSamples(T values[], int count)

{

m_Free = count < 100? count:100;

// Don’t exceed the array

for(int i = 0; i < m_Free; i++)

 

m_Values[i] = values[i];

// Store count number of samples

}

The class to which the constructor belongs is specified in the template in the same way as for an ordinary member function. Note that the constructor name doesn’t require the parameter specification(it is just CSamples, but it needs to be qualified by the class template type CSamples<T>. You only use the parameter with the class template name preceding the scope resolution operator.

Creating Objects from a Class Template

When you use a function defined by a function template, the compiler is able to generate the function from the types of the arguments used. The type parameter for the function template is implicitly defined by the specific use of a particular function. Class templates are a little different. To create an object based on a class template, you must always specify the type parameter following the class name in the declaration.

For example, to declare a CSamples<> object to handle samples of type double, you could write the declaration as:

CSamples<double> myData(10.0);

This defines an object of type CSamples<double> that can store samples of type double, and the object is created with one sample stored with the value 10.0.

Try It Out

Class Templating

You could create an object from the CSamples<> template that stores CBox objects. This works because the CBox class implements the operator>() function to overload the greater-than operator. You could exercise the class template with the main() function in the following listing:

//Ex8_07.cpp

//Using a class template #include <iostream>

431

Chapter 8

using std::cout; using std::endl;

//Put the CBox class definition from Ex8_06.cpp here...

//CSamples class template definition

template <class T> class CSamples

{

public:

// Constructors

CSamples(const T values[], int count); CSamples(const T& value);

CSamples(){ m_Free = 0; }

bool Add(const T& value);

// Insert a value

T Max() const;

// Calculate maximum

private:

 

T m_Values[100];

// Array to store samples

int m_Free;

// Index of free location in m_Values

};

// Constructor template definition to accept an array of samples template<class T> CSamples<T>::CSamples(const T values[], int count)

{

m_Free = count < 100? count:100;

// Don’t exceed the array

for(int i = 0; i < m_Free; i++)

 

m_Values[i] = values[i];

// Store count number of samples

}

 

// Constructor to accept a single sample

template<class T> CSamples<T>::CSamples(const T& value)

{

 

m_Values[0] = value;

// Store the sample

m_Free = 1;

// Next is free

}

 

// Function to add a sample

 

template<class T> bool CSamples<T>::Add(const T& value)

{

 

bool OK = m_Free < 100;

// Indicates there is a free place

if(OK)

 

m_Values[m_Free++] = value;

// OK true, so store the value

return OK;

 

}

 

// Function to obtain maximum sample

 

template<class T> T CSamples<T>::Max() const

{

 

T theMax = m_Free ? m_Values[0] : 0; // Set first sample or 0 as maximum

for(int i = 1; i < m_Free; i++)

// Check all the samples

if(m_Values[i] > theMax)

 

theMax = m_Values[i];

// Store any larger sample

return theMax;

 

432

 

More on Classes

 

 

}

 

int main()

 

{

 

CBox boxes[] = {

// Create an array of boxes

CBox(8.0, 5.0, 2.0),

// Initialize the boxes...

CBox(5.0, 4.0, 6.0),

 

CBox(4.0, 3.0, 3.0)

 

};

 

// Create the CSamples object to hold CBox objects CSamples<CBox> myBoxes(boxes, sizeof boxes / sizeof CBox);

CBox maxBox = myBoxes.Max();

// Get the biggest box

cout << endl

// and output its volume

<< “The biggest box has a volume of “

 

<< maxBox.Volume()

 

<< endl;

 

return 0;

 

}

 

You should replace the comment with the CBox class definition from Ex8_06.cpp earlier in the chapter. You don’t need to worry about the operator>() function that supports comparison of a CBox object with a value of type double because this example does not need it. With the exception of the default constructor, all the member functions of the template are defined by separate function templates, just to show you a complete example of how it’s done.

In main() you create an array of three CBox objects and then use this array to initialize a CSamples object that can store CBox objects. The declaration of the CSamples object is basically the same as it would be for an ordinary class, but with the addition of the type parameter in angled brackets following the template class name.

The program generates the following output:

The biggest box has a volume of 120

Note that when you create an instance of a class template, it does not follow that instance of the function templates for function members will also be created. The compiler only creates instances of templates for member functions that you actually call in your program. In fact, your function templates can even contain coding errors, and as long as you don’t call the member function that the template generates, the compiler does not complain. You can test this out with the example. Try introducing a few errors into the template for the Add() member. The program still compiles and run because it doesn’t call the Add() function.

You could try modifying the example and perhaps seeing what happens when you instantiate classes by using the template with various other types.

You might be surprised at what happens if you add some output statements to the class constructors. The constructor for the CBox is being called 103 times! Look at what is happening in the main() function. First you create an array of 3 CBox objects, so that’s 3 calls. You then create a CSamples object to hold them, but a CSamples object contains an array of 100 variables of type CBox, so you call the default constructor another 100 times, once for each element in the array. Of course, the maxBox object will be created by the default copy constructor that is supplied by the compiler.

433

Chapter 8

Class Templates with Multiple Parameters

Using multiple type parameters in a class template is a straightforward extension of the example using a single parameter that you have just seen. You can use each of the type parameters wherever you want in the template definition. For example, you could define a class template with two type parameters:

template<class T1, class T2> class CExampleClass

{

//Class data members private:

T1 m_Value1;

T2 m_Value2;

//Rest of the template definition...

};

The types of the two class data members shown are determined by the types you supply for the parameters when you instantiate an object.

The parameters in a class template aren’t limited to types. You can also use parameters that require constants or constant expressions to be substituted in the class definition. In our CSamples template, we arbitrarily defined the m_Values array with 100 elements. You could, however, let the user of the template choose the size of the array when the object is instantiated, by defining the template as:

template <class T, int Size> class CSamples

{

 

private:

 

T m_Values[Size];

// Array to store samples

int m_Free;

// Index of free location in m_Values

public:

// Constructor definition to accept an array of samples CSamples(const T values[], int count)

{

m_Free = count < Size? count:Size; // Don’t exceed the array

for(int i = 0; i < m_Free; i++)

 

m_Values[i] = values[i];

// Store count number of samples

}

 

// Constructor to accept a single sample

CSamples(const T& value)

 

{

 

m_Values[0] = value;

// Store the sample

m_Free = 1;

// Next is free

}

 

// Default constructor

 

CSamples()

 

{

 

m_Free = 0;

// Nothing stored, so first is free

434

 

 

More on Classes

 

}

 

 

// Function to add a sample

 

 

int Add(const T& value)

 

 

{

 

 

int OK = m_Free < Size;

// Indicates there is a free place

 

if(OK)

 

 

m_Values[m_Free++] = value;

// OK true, so store the value

 

return OK;

 

 

}

 

// Function to obtain maximum sample T Max() const

{

// Set first sample or 0 as maximum T theMax = m_Free ? m_Values[0] : 0;

for(int i = 1; i < m_Free; i++) // Check all the samples if(m_Values[i] > theMax)

theMax = m_Values[i]; // Store any larger sample return theMax;

}

};

The value supplied for Size when you create an object replaces the appearance of the parameter throughout the template definition. Now you can declare the CSamples object from the previous example as:

CSamples<CBox, 3> MyBoxes(boxes, sizeof boxes/sizeof CBox);

Because you can supply any constant expression for the Size parameter, you could also have written this as:

CSamples<CBox, sizeof boxes/sizeof CBox>

MyBoxes(boxes, sizeof boxes/sizeof CBox);

The example is a poor use of a template though(the original version was much more usable. A consequence of making Size a template parameter is that instances of the template that store the same types of objects but have different size parameter values are totally different classes and cannot be mixed. For instance, an object of type CSamples<double, 10> cannot be used in an expression with an object of type CSamples<double, 20>.

You need to be careful with expressions that involve comparison operators when instantiating templates. Look at this statement:

CSamples<aType, x > y ? 10 : 20 > MyType();

// Wrong!

This does not compile correctly because the > preceding y in the expression is interpreted as a rightangled bracket. Instead, you should write this statement as:

CSamples<aType, (x > y ? 10 : 20) > MyType();

// OK

435

Chapter 8

The parentheses ensure that the expression for the second template argument doesn’t get mixed up with the angled brackets.

Using Classes

I’ve touched on most of the basic aspects of defining a native C++ class, so maybe we should look at how a class might be used to solve a problem. I’ll need to keep the problem simple to keep this book down to a reasonable number of pages, so we’ll consider problems in which we can use an extended version of the CBox class.

The Idea of a Class Interface

The implementation of an extended CBox class should incorporate the notion of a class interface. We are going to provide a tool kit for anyone wanting to work with CBox objects so we need to assemble a set of functions that represents the interface to the world of boxes. Because the interface represents the only way to deal with CBox objects, it needs to be defined to cover adequately the likely things one would want to do with a CBox object, and be implemented, as far as possible, in a manner that protects against misuse or accidental errors.

The first question that you need to consider in designing a class is the nature of the problem you intend to solve and, from that, determine the kind of functionality you need to provide in the class interface.

Defining the Problem

The principal function of a box is to contain objects of one kind or another, so, in a word, the problem is packaging. We’ll attempt to provide a class that eases packaging problems in general and then see how it might be used. We assume that we’ll always be working on packing CBox objects into other CBox objects because, if you want to pack candy in a box, you can always represent each of the pieces of candy as an idealized CBox object. The basic operations that you might want to provide in the CBox class include:

Calculate the volume of a CBox. This is a fundamental characteristic of a CBox object and you have an implementation of this already.

Compare the volumes of two CBox objects to determine which is the larger. You probably should support a complete set of comparison operators for CBox objects. You already have a version of the > operator.

Compare the volume of a CBox object with a specified value and vice versa. You also have an implementation of this for the > operator, but you will also need to implement functions supporting the other comparison operators.

Add two CBox objects to produce a CBox object, which will contain both the original objects. Thus, the result will be at least the sum of the volumes, but may be larger. You have a version of this already that overloads the + operator.

Multiply a CBox object by an integer (and vice versa) to provide a CBox object, which will contain a specified number of the original objects. This is effectively designing a carton.

436

More on Classes

Determine how many CBox objects of a given size can be packed in another CBox object of a given size. This is effectively division, so you could implement this by overloading the / operator.

Determine the volume of space remaining in a CBox object after packing it with the maximum number of CBox objects of a given size.

I had better stop right there! There are undoubtedly other functions that would be very useful but, in the interest of saving trees, we’ll consider the set to be complete, apart from ancillaries such as accessing dimensions, for example.

Implementing the CBox Class

You really need to consider the degree of error protection that you want to build into the CBox class. The basic class that you defined to illustrate various aspects of classes is a starting point, but you should also consider some points a little more deeply. The constructor is a little weak in that it doesn’t ensure that the dimensions for a CBox are valid so perhaps the first thing you should do is to ensure that you always have valid objects. You could redefine the basic class as follows to do this:

class CBox

// Class definition at global scope

{

 

public:

 

// Constructor definition

CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0)

{

lv = lv <= 0?

1.0: lv;

// Ensure positive

wv = wv <= 0?

1.0: wv;

// dimensions for

hv = hv <= 0?

1.0: hv;

// the object

m_Length = lv

> wv? lv: wv;

// Ensure that

m_Width = wv

< lv? wv: lv;

// length >= width

m_Height = hv;

}

//Function to calculate the volume of a box double Volume() const

{

return m_Length*m_Width*m_Height;

}

//Function providing the length of a box double GetLength() const { return m_Length; }

//Function providing the width of a box double GetWidth() const { return m_Width; }

//Function providing the height of a box double GetHeight() const { return m_Height; }

private:

double m_Length; double m_Width; double m_Height;

};

//Length of a box in inches

//Width of a box in inches

//Height of a box in inches

437

Chapter 8

The constructor is now secure because any dimension that the user of the class tries to set to a negative number or zero is set to 1 in the constructor. You might also consider displaying a message for a negative or zero dimension because there’s obviously an error when this occurs, and arbitrarily and silently setting a dimension to 1 might not be the best solution.

The default copy constructor is satisfactory for our class because you have no dynamic memory allocation for data members, and the default assignment operator will also work as you would like. The default destructor also works perfectly well in this case so you don’t need to define it. Perhaps now you should consider what is required to support comparisons of objects of our class.

Comparing CBox Objects

You should include support for the operators >, >=, ==, < and <= so that they work with both operands as CBox objects, as well between a CBox object and a value of type double. You can implement these as ordinary global functions because they don’t need to be member functions. You can write the functions that compare the volumes of two CBox objects in terms of the functions that compare the volume of a CBox object with a double value, so let’s start with the latter. You can start by repeating the operator>() function that you had before:

// Function for testing if a constant is > a CBox object int operator>(const double& value, const CBox& aBox)

{

return value > aBox.Volume();

}

You can now write the operator<() function in a similar way:

// Function for testing if a constant is < CBox object int operator<(const double& value, const CBox& aBox)

{

return value < aBox.Volume();

}

You can code the implementations of the same operators with the arguments reversed in terms of the two functions you have just defined:

//Function for testing if CBox object is > a constant int operator>(const CBox& aBox, const double& value) { return value < aBox; }

//Function for testing if CBox object is < a constant int operator<(const CBox& aBox, const double& value) { return value > aBox; }

You just use the appropriate overloaded operator function that you wrote before, with the arguments from the call to the new function switched.

438