Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 52 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
52
Dung lượng
137,29 KB
Nội dung
209 z 516 #include <cstdlib> #include <iostream> using namespace std; // A proxy class for sums of vectors template<class, size_t> class MyVectorSum; template<class T, size_t N> class MyVector { T data[N]; public: MyVector<T,N>& operator=(const MyVector<T,N>& right) { for (size_t i = 0; i < N; ++i) data[i] = right.data[i]; return *this; } MyVector<T,N>& operator=(const MyVectorSum<T,N>& right); const T& operator[](size_t i) const { return data[i]; } T& operator[](size_t i) { return data[i]; } }; // Proxy class hold references; uses lazy addition template <class T, size_t N> class MyVectorSum { const MyVector<T,N>& left; const MyVector<T,N>& right; public: MyVectorSum(const MyVector<T,N>& lhs, const MyVector<T,N>& rhs) : left(lhs), right(rhs) {} T operator[](size_t i) const { return left[i] + right[i]; } }; // Operator to support v3 = v1 + v2 template<class T, size_t N> MyVector<T,N>& MyVector<T,N>::operator=(const MyVectorSum<T,N>& right) { for (size_t i = 0; i < N; ++i) data[i] = right[i]; return *this; } // operator+ just stores references template<class T, size_t N> inline MyVectorSum<T,N> operator+(const MyVector<T,N>& left, const MyVector<T,N>& right) { return MyVectorSum<T,N>(left, right); } // Convenience functions for the test program below template<class T, size_t N> void init(MyVector<T,N>& v) { for (size_t i = 0; i < N; ++i) v[i] = rand() % 100; } template<class T, size_t N> void print(MyVector<T,N>& v) { for (size_t i = 0; i < N; ++i) 210 z 516 cout << v[i] << ' '; cout << endl; } int main() { MyVector<int, 5> v1; init(v1); print(v1); MyVector<int, 5> v2; init(v2); print(v2); MyVector<int, 5> v3; v3 = v1 + v2; print(v3); MyVector<int, 5> v4; // Not yet supported: //! v4 = v1 + v2 + v3; } ///:~ Comment The MyVectorSum class does no computation when it is created; it merely holds references to the two vectors to be added. It is only when you access a component of a vector sum that it is calculated (see its operator[]( )). The overload of the assignment operator for MyVector that takes a MyVectorSum argument is for an expression such as: Comment v1 = v2 + v3; // add two vectors When the expression v1+v2 is evaluated, a MyVectorSum object is returned (or actually, inserted inline, since that operator+( ) is declared inline). This is a small, fixed-size object (it holds only two references). Then the assignment operator mentioned above is invoked: v3.operator=<int,5>(MyVectorSum<int,5>(v2, v3)); This assigns to each element of v3 the sum of the corresponding elements of v1 and v2, computed in real time. No temporary MyVector objects are created. This program does not support an expression that has more than two operands, however, such as v4 = v1 + v2 + v3; The reason is that after the first addition, a second addition is attempted: (v1 + v2) + v3; which would require an operator+( ) with a first argument of MyVectorSum and a second argument of type MyVector. We could attempt to provide a number of overloads to meet all situations, but it is better to let templates do the work, as in the following version of the program. Comment //: C05:MyVector2.cpp // Handles sums of any length with expression templates #include <cstddef> #include <cstdlib> #include <iostream> using namespace std; // A proxy class for sums of vectors template<class, size_t, class, class> class MyVectorSum; template<class T, size_t N> class MyVector { T data[N]; public: 211 z 516 MyVector<T,N>& operator=(const MyVector<T,N>& right) { for (size_t i = 0; i < N; ++i) data[i] = right.data[i]; return *this; } template<class Left, class Right> MyVector<T,N>& operator=(const MyVectorSum<T,N,Left,Right>& right); const T& operator[](size_t i) const { return data[i]; } T& operator[](size_t i) { return data[i]; } }; // Allows mixing MyVector and MyVectorSum template <class T, size_t N, class Left, class Right> class MyVectorSum { const Left& left; const Right& right; public: MyVectorSum(const Left& lhs, const Right& rhs) : left(lhs), right(rhs) {} T operator[](size_t i) const { return left[i] + right[i]; } }; template<class T, size_t N> template<class Left, class Right> MyVector<T,N>& MyVector<T,N>:: operator=(const MyVectorSum<T,N,Left,Right>& right) { for (size_t i = 0; i < N; ++i) data[i] = right[i]; return *this; } // operator+ just stores references template<class T, size_t N> inline MyVectorSum<T,N,MyVector<T,N>,MyVector<T,N> > operator+(const MyVector<T,N>& left, const MyVector<T,N>& right) { return MyVectorSum<T,N,MyVector<T,N>,MyVector<T,N> > (left,right); } template<class T, size_t N, class Left, class Right> inline MyVectorSum<T, N, MyVectorSum<T,N,Left,Right>, MyVector<T,N> > operator+(const MyVectorSum<T,N,Left,Right>& left, const MyVector<T,N>& right) { return MyVectorSum<T,N,MyVectorSum<T,N,Left,Right>, MyVector<T,N> > (left, right); } // Convenience functions for the test program below template<class T, size_t N> void init(MyVector<T,N>& v) { for (size_t i = 0; i < N; ++i) v[i] = rand() % 100; 212 z 516 } template<class T, size_t N> void print(MyVector<T,N>& v) { for (size_t i = 0; i < N; ++i) cout << v[i] << ' '; cout << endl; } int main() { MyVector<int, 5> v1; init(v1); print(v1); MyVector<int, 5> v2; init(v2); print(v2); MyVector<int, 5> v3; v3 = v1 + v2; print(v3); // Now supported: MyVector<int, 5> v4; v4 = v1 + v2 + v3; print(v4); MyVector<int, 5> v5; v5 = v1 + v2 + v3 + v4; print(v5); } ///:~ Comment Instead of committing ahead of time which types the arguments of a sum will be, we let the template facility deduce them with the template arguments, Left and Right. The MyVectorSum template takes these extra two parameters so it can represent a sum of any combination of pairs of MyVector and MyVectorSum. Note also that the assignment operator this time is a member function template. This also allows any <T, N> pair to be coupled with any <Left, Right> pair, so a MyVector object can be assigned from a MyVectorSum holding references to any possible pair of the types MyVector and MyVectorSum. As we did before, let’s trace through a sample assignment to understand exactly what takes place, beginning with the expression Comment v4 = v1 + v2 + v3; Since the resulting expressions become quite unwieldy, in the explanation that follows, we will use MVS as shorthand for MyVectorSum, and will omit the template arguments. Comment The first operation is v1+v2, which invokes the inline operator+( ), which in turn inserts MVS (v1, v2) into the compilation stream. This is then added to v3, which results in a temporary object according to the expression MVS(MVS(v1, v2), v3). The final representation of the entire statement is Comment v4.operator+(MVS(MVS(v1, v2), v3)); This transformation is all arranged by the compiler and explains why this technique carries the moniker “expression templates”; the template MyVectorSum represents an expression (an addition, in this case), and the nested calls above are reminiscent of the parse tree of the left- associative expression v1+v2+v3. Comment An excellent article by Angelika Langer and Klaus Kreft explains how this technique can be extended to more complex computations. Comment Template compilation models You have certainly noticed by now that all our template examples place fully-defined templates [76] 213 z 516 within each compilation unit. (For example, we place them completely within single-file programs or in header files for multi-file programs.) This runs counter to the conventional practice of separating ordinary function definitions from their declarations by placing the latter in header files and the function implementations in separate (that is, .cpp) files. Everyone knows the reason for this separation: non-inline function bodies in header files can lead to multiple function definitions, which results in a linker error. A nice side benefit of this approach is that vendors can distribute pre-compiled code along with headers so that users cannot see their function implementations, and compile times are shorter since header files are smaller. Comment The inclusion model Templates, on the other hand, are not code, per se, but instructions for code generation; only template instantiations are real code. When a compiler has seen a complete template definition during a compilation and then encounters a point of instantiation for that template in the same translation unit, it must deal with the fact that an equivalent point of instantiation may be present in another translation unit. The most common approach consists in generating the code for the instantiation in every translation unit and let the linker weed out duplicates. That particular approach also works well with inline functions that cannot be inlined and with virtual function tables, which is one of the reasons for its popularity. Nonetheless, several compilers prefer instead to rely on more complex schemes to avoid generating a particular instantiation more than once. Either way, it is the responsibility of the C++ translation system to avoid errors due to multiple equivalent points of instantiation. Comment A drawback of this approach is obviously that all template source code is visible to the client. If you want to know exactly how your standard library is implemented, all you have to do is inspect the headers in your installation. There is little opportunity for library vendors to hide their implementation strategies. Another noticeable disadvantage of the inclusion model is that header files are much, much larger than they would be if function bodies were compiled separately. This can increase compile times dramatically over traditional compilation models. Comment To help reduce the large headers required by the inclusion model, C++ offers two (non-exclusive) alternative code organization mechanisms: you can manually instantiate each specialization using explicit instantiation or you can use exported templates, which actually support a large degree of separate compilation. Comment Explicit instantiation You can manually direct the compiler to instantiate any template specializations of your choice. When you use this technique, there must be one and only one such directive for each such specialization; otherwise you might get multiple definition errors, just as you would with ordinary, non-inline functions with identical signatures. To illustrate, we first (erroneously) separate the declaration of the min template from earlier in this chapter from its definition, following the normal pattern for ordinary, non-inline functions. The following example consists of five files: Comment • OurMin.h: contains the declaration of the min function template. • OurMin.cpp: contains the definition of the min function template. • UseMin1.cpp: attempts to use an int-instantiation of min • UseMin2.cpp: attempts to use a double-instantiation of min • MinMain.cpp: calls usemin1( ) and usemin2( ) Here are the files: 214 z 516 //: C05:OurMin.h #ifndef OURMIN_H #define OURMIN_H // The declaration of min template<typename T> const T& min(const T&, const T&); #endif ///:~ // OurMin.cpp #include "OurMin.h" // The definition of min template<typename T> const T& min(const T& a, const T& b) { return (a < b) ? a : b; } //: C05:UseMin1.cpp {O} #include <iostream> #include "OurMin.h" void usemin1() { std::cout << min(1,2) << std::endl; } ///:~ //: C05:UseMin2.cpp {O} #include <iostream> #include "OurMin.h" void usemin2() { std::cout << min(3.1,4.2) << std::endl; } ///:~ //: C05:MinMain.cpp //{L} UseMin1 UseMin2 MinInstances void usemin1(); void usemin2(); int main() { usemin1(); usemin2(); } ///:~ When we attempt to build this program, the linker reports unresolved external references for min<int>( ) and min<double>( ). The reason is that when the compiler encounters the calls to specializations of min in UseMin1 and UseMin2, only the declaration of min is visible. Since the definition is not available, the compiler assumes it will come from some other translation unit, and the needed specializations are therefore not instantiated at that point, leaving the linker to eventually complain that it cannot find them. Comment To solve this problem, we will introduce a new file, MinInstances.cpp, that explicitly instantiates the needed specializations of min: //: C05:MinInstances.cpp {O} #include "OurMin.cpp" // Explicit Instantiations for int and double template const int& min<int>(const int&, const int&); template const double& min<double>(const double&, 215 z 516 const double&); ///:~ To manually instantiate a particular template specialization, you precede the specialization’s declaration with the template keyword. That’s it! Note that we must include OurMin.cpp, not OurMin.h, here, because the compiler needs the template definition to perform the instantiation. This is the only place where we have to do this in this program, however, since it gives us the unique instantiations of min that we need; the declarations alone suffice for the other files. Since we are including OurMin.cpp with the macro preprocessor, we add include guards: Comment //: C05:OurMin.cpp {O} #ifndef OURMIN_CPP #define OURMIN_CPP #include "OurMin.h" template<typename T> const T& min(const T& a, const T& b) { return (a < b) ? a : b; } #endif ///:~ Now when we compile all the files together into a complete program, the unique instances of min are found, and the program executes correctly, giving the output: 1 3.1 You can also manually instantiate classes and static data members. When explicitly instantiating a class, all member functions for the requested specialization are instantiated, except any that may have been explicitly instantiated previously. Using only implicit instantiation has the advantage here: only member functions that actually get called are instantiated. Explicit instantiation is intended for large projects in which a hefty chunk of compilation time can be avoided. Whether you use implicit or explicit instantiation is independent of which template compilation you use, of course; you can use manual instantiation with either the inclusion model or the separation model (discussed in the next section). Comment The separation model The separation model of template compilation allows you to separate function template definitions or static data member definitions from their declarations across translation units, just like you do with ordinary functions and data, by exporting templates. After reading the preceding two sections, this must sound strange indeed. Why bother to have the inclusion model in the first place if you can just adhere to the status quo? The reasons are both historical and technical. Comment Historically, the inclusion model was the first to experience widespread commercial use. Part of the reason for that was that the separation model was not well specified until late in the standardization process. It turns out that the inclusion model is the easier of the two to implement. All C++ compilers support the inclusion model. A lot of working code was in existence long before the semantics of the separation model were finalized. Comment The technical aspect reflects the fact that the separation model is difficult to implement. In fact, as of summer 2003 only one compiler front end (EDG) supports the separation model, and at the moment it still requires that template source code be available at compile time to perform instantiation on demand. Plans are in place to use some form of intermediate code instead of requiring that the original source be at hand, at which point you will be able to ship “pre- compiled” templates without shipping source code. Because of the lookup complexities explained earlier in this chapter (about dependent names being looked up in the template definition [77] 216 z 516 context), a full template definition still has to be available in some form when you compile a program that instantiates it. Comment The syntax to separate the source code of a template definition from its declaration is easy enough. You use the export keyword: // C05:OurMin2.h // Declares min as an exported template //! (Only works with EDG-based compilers) #ifndef OURMIN2_H #define OURMIN2_H export template<typename T> const T& min(const T&, const T&); #endif Similar to inline or virtual, the export keyword need only be mentioned once in a compilation stream, where an exported template is introduced. For this reason, we need not repeat it in the implementation file, but it is considered good practice to do so: Comment // C05:OurMin2.cpp // The definition of the exported min template //! (Only works with EDG-based compilers) #include "OurMin2.h" export template<typename T> const T& min(const T& a, const T& b) { return (a < b) ? a : b; } ///:~ The UseMin files used previously only need to be updated to include the correct header file (OurMin2.h), and the main program need not change at all. Although this appears to give true separation, the file with the template definition (OurMin2.cpp) must still be shipped to users (because it must be processed for each instantiation of min) until such time as some form of intermediate code representation of template definitions is supported. So while the standard does provide for a true separation model, not all of its benefits can be reaped today. Only one family of compilers currently support export (those based on the EDG front end), and these compilers currently do not exploit the potential ability to distribute template definitions in compiled form. Comment Summary Templates have gone far beyond simple type parameterization! When you combine argument type deduction, custom specialization, and template metaprogramming, C++ templates emerge as a powerful code generation mechanism. One of the weaknesses of C++ templates we skipped in this chapter is the difficulty in interpreting compile-time error messages. When you’re not used to it, the quantity of inscrutable text spewed out by the compiler is quite overwhelming. If it’s any consolation, C++ compilers have actually gotten a lot better about this. Leor Zolman has written a nifty tool STLFilt, that renders these error messages much more readable by extracting the useful information and throwing away the rest. Comment Another important idea to take away from this chapter is that a template implies an interface. That is, even though the template keyword says “I’ll take any type,” the code in a template definition actually requires that certain operators and member functions be supported—that’s the interface. So in reality, a template definition is saying, “I’ll take any type that supports this interface.” Things would be much nicer if the compiler could simply say, “Hey, this type that you’re trying to instantiate the template with doesn’t support that interface—can’t do it.” Using [78] 217 z 516 templates, therefore, constitutes a sort of “latent type checking” that is more flexible than the pure object-oriented practice of requiring all types to derive from certain base classes. Comment In Chapters 6 and 7 we explore in depth the most famous application of templates, the subset of the standard C++ library commonly known as the Standard Template Library (STL). Chapters 9 and 10 also use template techniques not found in this chapter. Comment Exercises 1. Write a unary function template that takes a single type template parameter. Create a full specialization for the type int. Also create a non-template overload for this function that takes a single int parameter. Have your main program invoke three function variations. 2. Write a class template that uses a vector to implement a stack data structure. 38. Modify your solution to the previous exercise so that the type of the container used to implement the stack is a template template parameter. 39. In the following code, the class NonComparable does not have an operator=( ). Why would the presence of the class HardLogic cause a compile error, but SoftLogic would not? class Noncomparable {}; struct HardLogic { Noncomparable nc1, nc2; void compare() { return nc1 == nc2; // Compiler error } }; template<class T> struct SoftLogic { Noncomparable nc1, nc2; void noOp() {} void compare() { nc1 == nc2; } }; int main() { SoftLogic<Noncomparable> l; l.noOp(); } 40. Write a function template that takes a single type parameter (T) and accepts four function arguments: an array of T, a start index, a stop index (inclusive), and an optional initial value. The function returns the sum of all the array elements in the specified range. Use the default constructor of T for the default initial value. 41. Repeat the previous exercise but use explicit instantiation to manually create specializations for int and double, following the technique explained in this chapter. 42. Why does the following code not compile? (Hint: what do class member functions have access to?) class Buddy {}; template<class T> class My { int i; public: void play(My<Buddy>& s) { 218 z 516 s.i = 3; } }; int main() { My<int> h; My<Buddy> me, bud; h.play(bud); me.play(bud); } 43. Why does the following code not compile? template<class T> double pythag(T a, T b, T c) { return (-b + sqrt(double(b*b - 4*a*c))) / 2*a; } int main() { pythag(1, 2, 3); pythag(1.0, 2.0, 3.0); pythag(1, 2.0, 3.0); pythag<double>(1, 2.0, 3.0); } 44. Write templates that take non-type parameters of the following variety: an int, a pointer to an int, a pointer to a static class member of type int, and a pointer to a static member function. 45. Write a class template that takes two type parameters. Define a partial specialization for the first parameter, and another partial specialization that specifies the second parameter. In each specialization, introduce members that are not in the primary template. 46. Define a class template named Bob that takes a single type parameter. Make Bob a friend of all instances of a template class named Friendly, and a friend of a class template named Picky only when the type parameter of Bob and Picky are identical. Give Bob member functions that demonstrate its friendship. Comment 6: Generic algorithms Algorithms are at the core of computing. To be able to write an algorithm once and for all to work with any type of sequence makes your programs both simpler and safer. The ability to customize algorithms at runtime has revolutionized software development. The subset of the standard C++ library known as the Standard Template Library (STL) was originally designed around generic algorithms—code that processes sequences of any type of values in a type-safe manner. The goal was to use predefined algorithms for almost every task, instead of hand-coding loops every time you need to process a collection of data. This power comes with a bit of a learning curve, however. By the time you get to the end of this chapter, you should be able to decide for yourself whether you find the algorithms addictive or too confusing to [...]... y, r, minus()): 0 -6 -5 27 11 -2 6 16 4 -1 9 17 After testBinary(x, y, r, multiplies()): 16 1 12 414 324 24 2 1 92 377 28 5 1100 1410 After testBinary(x, y, r, divides()): 100 420 2101 After testBinary(x, y, r, limit()): 0 8 18 0 0 6 3 4 25 17 After testUnary(x, r, negate()): -4 -8 -1 8 -3 6 -2 2 -6 -2 9 -1 9 -2 5 -4 7 After testBinary(x, y, br, equal_to()): 1000000000 After testBinary(x,... this way, copy( ) can duplicate a vector, as in the following example Comment //: C0 6:CopyVector.cpp // Copies the contents of a vector #include #include #include #include using namespace std; int main() { int a[] = {10, 20 , 30}; const size_t SIZE = sizeof a / sizeof a[0]; vector v1(a, a + SIZE); vector v2(SIZE); copy(v1.begin(), v1.end(), v2.begin());... apply Comment Name Type Result produced plus BinaryFunction arg1 + arg2 minus BinaryFunction arg1 - arg2 multiplies BinaryFunction arg1 * arg2 22 9 z 51 6 divides BinaryFunction arg1 / arg2 modulus BinaryFunction arg1 % arg2 negate UnaryFunction - arg1 equal_to BinaryPredicate arg1 == arg2 not_equal_to BinaryPredicate arg1 != arg2 greater BinaryPredicate arg1 > arg2 less BinaryPredicate arg1 < arg2 greater_equal... ostream_iterator(cout, " ")); cout = arg2 less_equal BinaryPredicate arg1 . new file, MinInstances.cpp, that explicitly instantiates the needed specializations of min: //: C0 5: MinInstances.cpp {O} #include "OurMin.cpp" // Explicit Instantiations for int and double template. Comment //: C0 6:CopyStrings.cpp // Copies strings #include <algorithm> #include <cassert> #include <cstddef> #include <string> using namespace std; int main() { string a[]. objects. They are admittedly simple, but you can use them to compose more complicated function objects. Consequently, in many instances, you can construct complicated predicates without writing