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

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

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

More about Program Structure

This catch block catches the exception because it now has the correct type.

To throw the exception so that it can be caught by the original catch block, you must change the code in the try block:

try

{

throw gcnew String(L”Catch me if you can.”);

}

catch(String^ ex) // OK. Exception thrown is of this type

{

Console::WriteLine(L”String^: {0}”,ex);

}

Now the exception is a String object and is thrown as type String^, a handle that references the string.

You can use function templates in your C++/CLI programs, but you have an additional capability called generic functions that looks remarkably similar; however, there are significant differences.

Understanding Generic Functions

Although generic functions appear to do the same thing as function templates and therefore at first sight seem superfluous, generic functions work rather differently from template functions and the differences make them a valuable additional capability in CLR programs. When you use a function template, the compiler generates the source code for functions that you require from the template; this generated code is then compiled along with the rest of your program code. In some cases this can result in many functions being generated and the size of the execution module may be increased substantially. On the other hand a generic function specification is itself compiled, and when you call a function that matches the generic function specification, actual types are substituted for the type parameters at execution time. No extra code is generated at compile time and the code-bloat that can arise with template functions does not occur.

Some aspects of defining generic function depend on a knowledge of stuff that comes in later chapters but it will al be clear eventually. I’ll provide minimal explanations here of the things that are new and you’ll learn the detail later in the book.

Defining Generic Functions

You define a generic function using type parameters that are replaced by actual types when the function is called. Here’s an example of a generic function definition:

generic<typename T> where T:IComparable T MaxElement(array<T>^ x)

{

T max = x[0];

for(int i = 1; i < x->Length; i++) if(max->CompareTo(x[i]) < 0)

max = x[i]; return max;

}

309

Chapter 6

This generic function does the same job as the native C++ function template you saw earlier in this chapter. The generic keyword in the first line identifies what follows as a generic specification and the first line defines the type parameter for the function as T; the typename keyword between the angled brackets indicates that the T that follows is the name of a type parameter in the generic function, and that this type parameter is replaced by an actual type when the generic function is used. For a generic function with multiple type parameters, the parameter names go between the angled brackets, each preceded by the typename keyword and separated by commas.

The where keyword that follows the closing angled bracket introduces a constraint on the actual type that may be substituted for T when the generic function is used. This particular constraint says that any type that is to replace T in the generic function must implement the IComparable interface. You’ll learn about interfaces later in the book, but for now I’ll say that it implies that the type must define the CompareTo() function that allows two objects of the type to be compared. Without this constraint the compiler has no knowledge of what operations are possible for the type that is to replace T because until the generic function is used, this is completely unknown. With the constraint you can use the CompareTo() function to compare max with an element of the array. The CompareTo() function returns an integer value that is less than 0 when the object for which it is called (max in this case) is less than the argument, zero if it equals the argument, and greater than 0 if it is greater than the argument.

The second line specifies the generic function name MaxElement, its return type T, and its parameter list. This looks rather like an ordinary function header except that it involves the generic type parameter T. The return type for the generic function and the array element type that is part of the parameter type specification are both of type T, so both these types are determined when the generic function is used.

Using Generic Functions

The simplest way of calling a generic function is just to use it like any ordinary function. For example, you could use the generic MaxElement() from the previous section like this:

array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1};

double maxData = MaxElement(data);

The compiler is able to deduce that the type argument to the generic function is double in this instance and generates the code to call the function accordingly. The function executes with instances of T in the function being replaced by double. As I said earlier, this is not like a template function; there is no creation of function instances at compile time. The compiled generic function is able to handle type argument substitutions when it is called.

Note that if you pass a string literal as a argument to a generic function, the compiler deduces that the type argument is String^, regardless of whether the string literal is a narrow string constant such as “Hello!” or a wide string constant such as L”Hello!”.

It is possible that the compiler may not be able to deduce the type argument from a call of a generic function. In these instances you can specify the type argument(s) explicitly between angled brackets following the function name in the call. For example, you could write the call in the previous fragment as:

double maxData = MaxElement<double>(data);

With an explicit type argument specified there is no possibility of ambiguity.

310

More about Program Structure

There are limitations on what types you can supply as a type argument to a generic function. A type argument cannot be a native C++ class type, or a native pointer or reference, or a handle to a value class type such as int^. Thus only value class types such as int or double and tracking handles such as String^ are allowed (but not a handle to a value class type).

Let’s try a working example.

Try It Out

Using a Generic Function

Here’s an example that defines and uses three generic functions:

//Ex6_10A.cpp : main project file.

//Defining and using generic functions

#include “stdafx.h”

using namespace System;

//Generic function to find the maximum element in an array generic<typename T> where T:IComparable

T MaxElement(array<T>^ x)

{

T max = x[0];

for(int i = 1; i < x->Length; i++) if(max->CompareTo(x[i]) < 0)

max = x[i]; return max;

}

//Generic function to remove an element from an array generic<typename T> where T:IComparable

array<T>^ RemoveElement(T element, array<T>^ data)

{

array<T>^ newData = gcnew array<T>(data->Length - 1);

int index = 0;

// Index to elements in newData array

bool found = false;

// Indicates that the element to remove was

found

for each(T item in data)

{

// Check for invalid index or element found if((!found) && item->CompareTo(element) == 0)

{

found = true; continue;

}

else

{

if(index == newData->Length)

{

Console::WriteLine(L”Element to remove not found”); return data;

}

newData[index++] = item;

311

Chapter 6

}

}

return newData;

}

// Generic function to list an array generic<typename T> where T:IComparable void ListElements(array<T>^ data)

{

for each(T item in data) Console::Write(L”{0,10}”, item);

Console::WriteLine();

}

int main(array<System::String ^> ^args)

{

array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; Console::WriteLine(L”Array contains:”); ListElements(data);

Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(data)); array<double>^ result = RemoveElement(MaxElement(data), data); Console::WriteLine(L” After removing maximum, array contains:”); ListElements(result);

array<int>^ numbers = {3, 12, 7, 0, 10,11}; Console::WriteLine(L”\nArray contains:”); ListElements(numbers);

Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(numbers)); Console::WriteLine(L”\nAfter removing maximum, array contains:”); ListElements(RemoveElement(MaxElement(numbers), numbers));

array<String^>^ strings = {L”Many”, L”hands”, L”make”, L”light”, L”work”}; Console::WriteLine(L”\nArray contains:”);

ListElements(strings);

Console::WriteLine(L”\nMaximum element = {0}\n”, MaxElement(strings)); Console::WriteLine(L”\nAfter removing maximum, array contains:”); ListElements(RemoveElement(MaxElement(strings), strings));

return 0;

}

The output from this example is:

Array contains:

 

 

 

 

1.5

3.5

6.7

4.2

2.1

Maximum element

= 6.7

 

 

 

After removing

maximum, array contains:

 

1.5

3.5

4.2

2.1

 

Array contains:

312

More about Program Structure

3

12

7

0

10

11

Maximum element = 12

 

 

 

 

After removing maximum, array contains:

 

 

3

7

0

10

11

 

Array contains:

 

 

 

 

 

Many

hands

make

light

work

 

Maximum element = work

After removing maximum, array contains:

Many hands make light

Press any key to continue . . .

How It Works

The first generic function that this example defines is MaxElement(), which is identical to the one you saw in the preceding section that finds the maximum element in an array so I won’t discuss it again.

The next generic function, RemoveElements(), removes the element passed as the first argument from the array specified by the second argument and the function returns a handle to the new array that results from the operation. You can see from the first two lines of the function definition that both parameter types and the return type involve the type parameter, T.

generic<typename T> where T:IComparable

array<T>^ RemoveElement(T element, array<T>^ data)

The constraint on T is the same as for the first generic function and implies that whatever type is used as the type argument must implement the CompareTo() function to allow objects of the type to be compared. The second parameter and the return type are both handles to an array of elements of type T. The first parameter is simply type T.

The function first creates an array to hold the result:

array<T>^ newData = gcnew array<T>(data->Length - 1);

The newData array is of the same type as the second argument(an array of elements of type T(but has one fewer elements because one element is to be removed from the original.

The elements are copied from the data array to the newData array in the for each loop:

int index = 0;

// Index to elements in newData array

bool found = false;

// Indicates that element to remove was

found

for each(T item in data)

{

// Check for invalid index or element found if((!found) && item->CompareTo(element) == 0)

313

Chapter 6

{

found = true; continue;

}

else

{

if(index == newData->Length)

{

Console::WriteLine(L”Element to remove not found”); return data;

}

newData[index++] = item;

}

}

All elements are copied except the one identified by the first argument to the function. You use the index variable to select the next newData array element that is to receive the next element from the data array. Each element from data is copied unless it is equal to element, in which case found is set to true and the continue statement skips to the next iteration. It is quite possible that an array could have more than one element equal to the first argument and the found variable prevents subsequent elements that are the same as element from being skipped in the loop.

You also have a check for the index variable exceeding the legal limit for indexing elements in newData. This could arise if there is no element in the data array equal to the first argument to the function. In this eventuality, you just return the handle to the original array.

The third generic function just lists the elements from an array of type array<T>:

generic<typename T>

void ListElements(array<T>^ data)

{

for each(T item in data) Console::Write(L”{0,10}”, item);

Console::WriteLine();

}

This is one of the few occasions where no constraint on the type parameter is needed. There is very little you can do with objects that are of a completely unknown type so typically generic function type parameters will have constraints. The operation of the function is very simple — each element from the array is written to the command line in an output field with a width of 10 in the for each loop. If you want, you could add a little sophistication by adding a parameter for the field width and creating the format string to be used as the first argument to the Write() function in the Console class. You could also add logic to the loop to write a specific number of elements per line based on the field width.

The main() function exercises these generic functions with type parameters of types double, int, and String^. Thus you can see that all three generic functions work with value types and handles. In the second and third examples, the generic functions are used in combination in a single statement. For example, look at this statement in the third sample use of the functions:

ListElements(RemoveElement(MaxElement(strings), strings));

The first argument to the RemoveElement() generic function is produced by the MaxElement() generic function call, so these generic function can be used in the same way as equivalent ordinary functions.

314

More about Program Structure

The compiler is able to deduce the type argument in all instances where the generic functions are used but you could specify the explicitly if you wanted to. For example, you could write the previous statement like this:

ListElements(RemoveElement<String^>(MaxElement<String^>(strings), strings));

A Calculator Program for the CLR

Let’s re-implement the calculator as a C++/CLI example. We’ll assume the same program structure and hierarchy of functions as you saw for the native C++ program but the functions will be declared and defined as C++/CLI functions. A good starting point is the set of function prototypes at the beginning of the source file(I have used Ex6_11 as the CLR project name:

//Ex6_11.cpp : main project file.

//A CLR calculator supporting parentheses

#include

“stdafx.h”

 

#include

<cstdlib>

// For exit()

using namespace System; String^ eatspaces(String^ str); double expr(String^ str);

double term(String^ str, int^ index); double number(String^ str, int^ index); String^ extract(String^ str, int^ index);

//Function to eliminate blanks

//Function evaluating an expression

//Function analyzing a term

//Function to recognize a number

//Function to extract a substring

All the parameters are now handles; the string parameters are type String^ and the index parameter that records the current position in the string is also a handle of type int^. Of course, a string is returned as a handle of type String^.

The main() function implementation looks like this:

int main(array<System::String ^> ^args)

{

String^ buffer;

// Input area for expression to be evaluated

Console::WriteLine(L”Welcome to your friendly calculator.”); Console::WriteLine(L”Enter an expression, or an empty line to quit.”);

for(;;)

{

buffer = eatspaces(Console::ReadLine());

// Read an input line

if(String::IsNullOrEmpty(buffer))

// Empty line ends calculator

return 0;

 

Console::WriteLine(L” = {0}\n\n”,expr(buffer)); // Output value of expression

}

return 0;

}

315

Chapter 6

The function is a lot shorter and easier to read. Within the indefinite for loop you call the String class function, IsNullOrEmpty(). This function returns true if the string passed as the argument is null or of zero length so it does exactly what you want here.

Removing Spaces from the Input String

The function to remove spaces is also simpler and shorter:

// Function to eliminate spaces from a string String^ eatspaces(String^ str)

{

// Array to hold string without spaces

array<wchar_t>^ chars = gcnew array<wchar_t>(str->Length);

int length = 0; // Number of chars in array

//Copy non-space characters to chars array for each(wchar_t ch in str)

if(ch != ‘ ‘) chars[length++] = ch;

//Return chars array as string

return gcnew String(chars, 0, length);

}

You first create an array to accommodate the string when the spaces have been removed. This is an array of elements of type wchar_t because strings in C++/CLI are Unicode characters. The process for removing the spaces is very simple — you copy all the characters that are not spaces from the string str to the array, chars, keeping track of the number of characters copied in the length variable. You finally create a new String object using a String class constructor that creates the object from elements in an array.

The first argument to the constructor is the array that is the source of characters for the string, the second argument is the index position for the first character from the array that forms the string, and the third argument is the total number of characters from the array that are to be used. The String class defines a range of constructors for creating strings in various ways.

Evaluating an Arithmetic Expression

You can implement the function to evaluate an expression like this:

// Function to evaluate an arithmetic expression

double expr(String^ str)

 

{

 

int^ index = 0;

// Keeps track of current character position

double value = term(str, index);

// Get first term

while(*index < str->Length)

 

{

 

switch(str[*index])

// Choose action based on current character

{

 

case ‘+’:

// + found so

++(*index);

// increment index and add

value += term(str, index);

// the next term

316

 

More about Program Structure

 

 

break;

 

case ‘-’:

// - found so

++(*index);

// decrement index and add

value -= term(str, index);

// the next term

break;

 

default:

// If we reach here the string is junk

Console::WriteLine(L”Arrrgh!*#!! There’s an error.\n”); exit(1);

}

}

return value;

}

The index variable is declared as a handle because you want to pass it to the term() function and allow the term() function to modify the original variable. If you declared index simply as type int, the term() function would receive a copy of the value and could not refer to the original variable to change it.

The declaration of index results in a warning message from the compiler because the statement relies on autoboxing of the value 0 to produce the Int32 value class object that the handle references and the warning is because people often write the statement like this but intend to initialize the handle to null. Of course, to do this you must use nullptr as the initial value in place of 0. If you want to eliminate the warning, you could rewrite the statement as:

int^ index = gcnew int(0);

This statement uses a constructor explicitly to create the object and initialize it with 0 so there’s no warning from the compiler.

After processing the initial term by calling the term() function the while loop steps through the string looking for + or - operators followed by another term. The switch statement identifies and processes the operators. If you have been using native C++ for a while, you might be tempted to write the case statements in the switch a little differently(for example:

// Incorrect code!! Does not work!!

 

case ‘+’:

// + found so

value += term(str, ++(*index));

// increment index & add the next term

break;

 

 

 

Of course, this would typically be written without the first comment. This code is wrong, but why is it wrong? The term() function expects a handle in type int^ as the second argument and that is

what is supplied here although maybe not what you expect. The compiler arranges for the expression ++(*index) to be evaluated and the result stored in a temporary location. The expression indeed updates the value referenced by index, but the handle that is passed to the term() function is a handle to the temporary location holding the result of evaluating the expression, not the handle index. This handle is produced by autoboxing the value stored in the temporary location. When the term() function updates, the value referenced by the handle that is passed to it the temporary location is updated, not the location referenced by index; thus all the updates to the index position of the string made in the term() function are lost. If you expect a function to update a variable in the calling program, you must not use an expression as the function argument — you must always use the handle variable name.

317

Chapter 6

Obtaining the Value of a Term

As in the native C++ version, the term() function steps through the string that is passed as the first argument, starting at the character position referenced by the second argument:

// Function to get the value of a term

 

double term(String^ str, int^ index)

 

{

 

double value = number(str, index);

// Get the first number in the term

// Loop as long as we have characters and a good operator

while(*index < str->Length)

 

{

 

if(str[*index] == L’*’)

// If it’s multiply,

{

 

++(*index);

// increment index and

value *= number(str, index);

// multiply by next number

}

 

else if( str[*index] == L’/’)

// If it’s divide

{

 

++(*index);

// increment index and

value /= number(str, index);

// divide by next number

}

 

else

 

break;

// Exit the loop

}

 

// We’ve finished, so return what we’ve got return value;

}

After calling the number() function to get the value of the first number or parenthesized expression in a term, the function steps through the string in the while loop. The loop continues while there are still characters in the input string as long as a * or / operators followed by another number or parenthesized expression is found.

Evaluating a Number

The number function extracts and evaluates a parenthesized expression if there is one; otherwise, it determines the value of the next number in the input:

// Function to recognize a number

 

double number(String^ str, int^ index)

 

{

 

double value = 0.0;

// Store for the resulting value

// Check for expression between parentheses

if(str[*index] == L’(‘ ) // Start of parentheses

{

++(*index);

String^ substr = extract(str, index); return expr(substr);

}

//Extract substring in brackets

//Return substring value

// Loop accumulating leading digits

318