Parameterized Functions and Types

32 297 0
Parameterized Functions and Types

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

285 ■ ■ ■ CHAPTER 11 Parameterized Functions and Types A function or type is said to be parameterized when one or more types used in the declara- tion and definition are left unspecified, so that users of the type can substitute the types of their choice. Parameterized functions are functions that have a type parameter in their argument list (or at least the return type). There are two types of parameterized types in C++/CLI: templates, which are inherited from C++, and generics, which are the CLI parameterized type. This chapter will explore generics in detail, look at some useful collection classes and container types, and then look at managed templates and compare them with generics. It will also discuss when to use generics and when to use managed templates. The syntax for generics is quite similar to that of templates. If you’re familiar with the template syntax, some of the description of the syntax for generics in the first few sections of this chapter may be old hat. Generics The main question you may have is why generics were introduced to the C++/CLI language when templates already existed in C++. First, the CLI already supported generics, and it was necessary to be able to access these in C++/CLI. Second, generics are really different from templates in fundamental ways, and hence have different uses. Once compiled, templates cease to be parameterized types. From the point of view of the runtime, the type created from a template is just another type. You can’t substitute a new type argument that wasn’t already used as an argument for that template at compile time. Generics are fundamentally different because they remain generic at runtime, so you can use types that were not known at compile time as type arguments. However, generics, like templates, have limitations that make them unsuitable for certain uses, as you’ll see later in this chapter. Let’s look at how to use generics. Type Parameters Generic functions and types are declared with the contextual keyword generic, followed by angle brackets and a list of type parameters with the keyword typename or class. As with template declarations, both typename and class are equivalent, even if the type argument used is not a class. Type parameters are identifiers, and thus follow the same rules as other identifiers such Hogenson_705-2C11.fm Page 285 Wednesday, October 18, 2006 5:09 PM 286 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES as variable names. The type parameter identifier is used as a placeholder for the type in the function or type definition. Listing 11-1 shows a generic function declaration. Listing 11-1. Declaring a Generic Function generic <typename T> void F(T t, int i, String^ s) { // . } This declaration creates a generic function, F, that takes three arguments, the first of which is an unspecified type. If more than one type parameter is to be used, they appear in a comma- separated list, as shown in Listing 11-2. Listing 11-2. Declaring Multiple Generic Parameters generic <typename T, typename U> void F(T t, array<U>^ a, int i, String^ s) { // . } The type parameter in a generic class or function can be used anywhere a type is used, for example, directly as a parameter or in aggregate type such as an array. The type parameter is capable of standing in for both value types as well as reference types. Generic Functions Generic functions are declared, defined, and used as in Listing 11-3. Listing 11-3. Declaring, Defining, and Using a Generic Function // generic_functions1.cpp using namespace System; generic < typename T> void GenericFunction(T t) { Console::WriteLine(t); } int main() { int i; // Specify the type parameter. GenericFunction<int>( 200 ); Hogenson_705-2C11.fm Page 286 Wednesday, October 18, 2006 5:09 PM CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES 287 // Allow the type parameter to be // deduced. GenericFunction( 400 ); } As you can see in this example, the generic function is called by using the function name, possibly followed by angle brackets and the type arguments to be substituted for the type parameters. I say “possibly” because if type arguments are omitted, the compiler attempts to deduce them from the types supplied to the function as arguments. For example, if a generic function takes one parameter and the type of that parameter is a type parameter, and if the type of the object supplied is, say, String, the type argument is assumed to be String and may be omitted. The type parameter need not be an actual argument type; however, it must appear in the argument list or as the return value. It may appear in a compound type, such as an array, as in Listing 11-4. Listing 11-4. Using a Generic Array As a Parameter // generic_functions2.cpp using namespace System; generic < typename T> void GenericFunction(array<T>^ array_of_t) { for each (T t in array_of_t) { Console::WriteLine(t); } } int main() { array<String^>^ array_of_string; array_of_string = gcnew array<String^> { "abc", "def", "ghi" }; // Allow the type parameter to be // deduced. GenericFunction( array_of_string ); } While deduction works on compound types, it doesn’t work if the type is used as a return value. The compiler won’t try to deduce the generic type argument from the left side of an assignment, or any other use of a return value. When only the return value of a generic function is generic, or if the generic type parameter doesn’t even appear in the function signature, the type argument must be explicitly specified, as in Listing 11-5. Hogenson_705-2C11.fm Page 287 Wednesday, October 18, 2006 5:09 PM 288 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES Listing 11-5. Explicitly Specifying a Type Argument // generic_return_value.cpp using namespace System; generic <typename T> T f() { return T(); } int main() { int i = f<int>(); // OK String^ s = f<String^>(); // OK double d = f(); // Error! Can't deduce type. } Generic Types Like generic functions, the declaration of a generic type differs from a nongeneric declaration by the appearance of the contextual keyword generic followed by the type parameter list. The type parameter may then be used in the generic definition wherever a type is used, for example, as a field, in a method signature as an argument type or return value, or as the type of a property, as shown in Listing 11-6. Listing 11-6. Using a Generic Type // generic_class1.cpp using namespace System; generic <typename T> ref class R { T t; public: R() {} property T InnerValue { T get() { return t; } void set(T value) { t = value; } } }; Hogenson_705-2C11.fm Page 288 Wednesday, October 18, 2006 5:09 PM CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES 289 int main() { double d = 0.01; int n = 12; // Create an object with T equal to double. R<double>^ r_double = gcnew R<double>(); // Create an object with T equal to int. R<int>^ r_int = gcnew R<int>(); r_double->InnerValue = d; r_int->InnerValue = n; Console::WriteLine( r_double->InnerValue ); Console::WriteLine( r_int->InnerValue ); } The types created from a generic type, such as R<double> and R<int> in Listing 11-6, are referred to as constructed types. Two or more types constructed from the same generic type are considered to be unique, unrelated types. Thus, you cannot convert from R<double> to R<int>. When a generic class or function is compiled, a generic version of that function or class is inserted into the assembly or module created for that source code. At runtime, constructed types are created on demand. Thus, it is not necessary to know at compile time all the possible types that might be used as type parameters. However, this freedom also means that the compile- time restrictions must be greater; otherwise, you would risk adding an incompatible type in at runtime, which might not have all the features required. When the compiler interprets the code for a generic class, it only allows methods, properties and other constructs to be called on the unknown type that are certain to be available. This ensures the type safety of generic types, since otherwise it would be possible to create a generic type that compiled but failed at runtime when the method used was not available. This restriction imposes constraints on the code you can use in your generic functions and types. For example, the code in Listing 11-7 won’t compile. Listing 11-7. Compiler Restrictions on Generic Types // invalid_use_of_type_param.cpp generic <typename T> ref class G { T t; public: Hogenson_705-2C11.fm Page 289 Wednesday, October 18, 2006 5:09 PM 290 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES G() { t = gcnew T("abc", 100); // Error: T may not have // a compatible constructor. t->F(); // Error: T may not have F. } }; Listing 11-7 will produce the compiler error: invalid_use_of_type_param.cpp(12) : error C3227: 'T' : cannot use 'gcnew' to allocate a generic type invalid_use_of_type_param.cpp(14) : error C2039: 'F' : is not a member of 'System::Object' c:\windows\microsoft.net\framework\v2.0.50727\mscorlib.dll : see declaration of 'System::Object' As you can see, the first complaint is that gcnew is not available on a generic type parameter; the second error occurs because the compiler is only willing to allow methods that are available on System::Object. There is a way to get around these restrictions. If you need to use specific features of a type, you must constrain the generic so that only types with those features are allowed to be used as type arguments. You’ll see how to do that in the section “Using Constraints.” But first, let’s look at a typical generic class implementing a simple collection. Generic Collections Generics are most often used to implement collection classes. Generic collection classes are more type-safe and can be faster than the alternative—nongeneric collection classes relying on handles to Object to represent items in the collection. The main efficiency gain is that the retrieval of items from the collection can be done without the use of casts, which usually requires a dynamic type check when the type is retrieved from the collection, or maybe even when adding elements to the collection. Also, if you are using value types, you can often avoid boxing and unboxing entirely by using a generic collection class. In addition to efficiency gains, if you use a generic type, you automatically force the objects in the collection to be of the appropriate type. Since most collections hold objects of the same type (or perhaps types with a common base type), this helps avoid programmatic errors involving adding objects of the wrong type to the collection. In addition, having the strongly typed collection leaves no doubt as to type needed, which is a relief to anyone who has had to try to figure out what type(s) a poorly documented, weakly typed collection takes. In order to use the for each statement on a generic collection, the collection must imple- ment the IEnumerable interface, and you must implement an enumerator class to walk through each element of the collection. Listing 11-8 shows the use of generics to create a linked list class that supports the for each statement to iterate through the generic collection. The generic collection implements IEnumerable, and an enumerator class implementing the IEnumerator interface is created to allow the for each statement to work. Hogenson_705-2C11.fm Page 290 Wednesday, October 18, 2006 5:09 PM CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES 291 Listing 11-8. Creating a Linked List That Can Be Traversed with for each // generic_list.cpp using namespace System; using namespace System::Collections::Generic; // ListNode represents a single element in a linked list. generic <typename T> ref struct ListNode { ListNode<T>(T t) : item(t) { } // The item field represents the data in the list. T item; // the next node in the list; ListNode<T>^ next; }; // List represents a linked list. generic <typename T> ref class MyList : IEnumerable<ListNode<T>^> { ListNode<T>^ first; public: property bool changed; // Add an item to the end of the list. void Add(T t) { changed = true; if (first == nullptr) { first = gcnew ListNode<T>(t); } else { // Find the end. ListNode<T>^ node = first; while (node->next != nullptr) { node = node->next; } node->next = gcnew ListNode<T>(t); } } Hogenson_705-2C11.fm Page 291 Wednesday, October 18, 2006 5:09 PM 292 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES // Return true if the object was removed, // false if it was not found. bool Remove(T t) { changed = true; if (first == nullptr) return false; if (first->item->Equals(t)) { // Remove first from list by // resetting first. first = first->next; return true; } ListNode<T>^ node = first; while(node->next != nullptr) { if (node->next->item->Equals(t)) { // Remove next from list by // leapfrogging it. node->next = node->next->next; return true; } node = node->next; } return false; } property ListNode<T>^ First { ListNode<T>^ get() { return first; } } private: virtual System::Collections::IEnumerator^ GetEnumerator_NG() sealed = System::Collections::IEnumerable::GetEnumerator { return GetEnumerator(); } Hogenson_705-2C11.fm Page 292 Wednesday, October 18, 2006 5:09 PM CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES 293 virtual IEnumerator<ListNode<T>^>^ GetEnumerator_G() sealed = IEnumerable<ListNode<T>^>::GetEnumerator { return GetEnumerator(); } public: IEnumerator<ListNode<T>^>^ GetEnumerator() { ListEnumerator<T>^ enumerator = gcnew ListEnumerator<T>(this); return (IEnumerator<ListNode<T>^>^) enumerator; } // ListEnumerator is a struct that walks the list, pointing // to each element in turn. generic <typename T> ref struct ListEnumerator : IEnumerator<ListNode<T>^> { ListNode<T>^ current; MyList<T>^ theList; bool beginning; ListEnumerator<T>(MyList<T>^ list) : theList(list), beginning(true) { theList->changed = false; } private: virtual property Object^ Current_NG { Object^ get() sealed = System::Collections::IEnumerator::Current::get { return (Object^) Current; } } virtual property ListNode<T>^ Current_G { ListNode<T>^ get() sealed = IEnumerator<ListNode<T>^>::Current::get { return Current; } } public: Hogenson_705-2C11.fm Page 293 Wednesday, October 18, 2006 5:09 PM 294 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES property ListNode<T>^ Current { ListNode<T>^ get() { if (theList->changed) throw gcnew InvalidOperationException(); return current; } } virtual bool MoveNext() { if (theList->changed) throw gcnew InvalidOperationException(); beginning = false; if (current != nullptr) { current = current->next; } else current = theList->First; if (current != nullptr) return true; else return false; } virtual void Reset() { theList->changed = false; current = theList->First; } ~ListEnumerator() {} }; // end of ListEnumerator }; // end of MyList int main() { MyList<int>^ int_list = gcnew MyList<int>(); int_list->Add(10); int_list->Add(100); int_list->Add(1000); int_list->Add(100000); Hogenson_705-2C11.fm Page 294 Wednesday, October 18, 2006 5:09 PM [...]... you proceed to them, let’s look at reference types and value types in generic types and functions, which will give a better idea of why the other constraint types are needed Reference Types and Value Types As Type Parameters Although the type parameter is written without a handle or any other adornment, when a type argument is supplied, it will either be a handle to a reference type or a value type The... parameterized types: generics and templates You saw how to declare, define, and use generic functions and types, and how to use constraints to allow generic code to use specific features of a specified subset of types You also looked at a variety of NET Framework collection classes, including ArrayList and Dictionary, and their associated helper classes You learned the differences between the generic and nongeneric... October 18, 2006 5:09 PM CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES In a similar manner, you can use all the other features of templates on your managed reference types, interfaces, and value types Some managed types cannot be templates: you cannot declare template enum classes or delegate types Otherwise, you can use nontype template parameters, you can use template functions, you can use template arguments,... CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES Using the Collection Class Interfaces The NET Framework collection classes have associated interfaces It is a good idea to use the interfaces instead of the types directly That way, code written for one collection type will work with other related collection types that implement the same interface Collection types use different data structures and algorithms,... being used If the objects in the nodes are value types, they are automatically owned by the collection, and they will get destroyed when the containing 301 Hogenson_705-2C11.fm Page 302 Wednesday, October 18, 2006 5:09 PM 302 CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES node is destroyed as part of the natural semantics of value types Since they’re value types, you never have to worry about another object... value types when you declare a value type constraint If you don’t, you have to write your class to handle both reference types and value types, and you have to deal with the question of object ownership As an example, take the MyList generic class used previously and change the Remove method to delete objects upon removal, as in Listing 11-15 Listing 11-15 A Collection That Owns the Objects and Deletes... classes and when to use them, and you also learned another way to enumerate over collections using enumerators and the for each statement Finally, you looked at managed templates and the differences between them and generics and you saw when to use one or the other In the next and final chapter, I’ll cover how to use C++/CLI to interoperate with other technologies, including other NET languages and native... CHAPTER 11 ■ PARAMETERIZED FUNCTIONS AND TYPES return gcnew T(); } ref class R { public: R() { } }; int main() { int i = CreateInstance(); R^ r = CreateInstance(); } The gcnew constraint is useful, but you cannot specify a specific constructor other than the default constructor Value Type Constraints Unconstrained type parameters can be either reference types or value types, and the same constructs... 11 ■ PARAMETERIZED FUNCTIONS AND TYPES • Templates support nontype template parameters; generics don’t • Templates support specialization and partial specialization; generics don’t • Templates work better with mathematical operations; unconstrained generics don’t allow the use of mathematical operators on the unknown type parameter, and there are no viable constraints for families of primitive types. .. IKey and the values to be value types The NET Framework BCL Dictionary class doesn’t have these restrictions You’ll learn more about the ArrayList and Dictionary collection classes in the next section .NET Framework Container Types Containers, or collection classes, are types that hold objects, provide ways to access them, and may be used in a variety of algorithms Lists, queues, stacks, trees, and . CHAPTER 11 Parameterized Functions and Types A function or type is said to be parameterized when one or more types used in the declara- tion and definition. There are other types of constraints, but before you proceed to them, let’s look at reference types and value types in generic types and functions, which

Ngày đăng: 05/10/2013, 08:20

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan