Templates Consider the following function which swaps two

  • Slides: 38
Download presentation
Templates Consider the following function, which swaps two integers: void swap(int &x, int &y)

Templates Consider the following function, which swaps two integers: void swap(int &x, int &y) { int temp = x; x = y; y = temp; } int i = 3, j = 4; swap(i, j);

void swap(int &x, int &y) { int temp = x; x = y; y

void swap(int &x, int &y) { int temp = x; x = y; y = temp; } Now suppose we also want a function to swap floats. Then we write another function (we can overload the name “swap” for different argument types): void swap(float &x, float &y) { float temp = x; x = y; y = temp; } float f = 4. 0, g = 5. 0; swap(f, g);

But writing the same code over and over for every possible type can get

But writing the same code over and over for every possible type can get tiring fast. What we’d really like would be a single swap function that worked for all types. Let’s express what we want as follows: // For any type T, swap two elements of type T: void swap(T &x, T &y) { T temp = x; x = y; y = temp; } swap is supposed to take an two elements of some type (let’s call this type T for the moment), and swap them. But we don’t know what T is when we write swap - ideally, swap should work for all possible types T.

C++ supports template declarations which express exactly what we want: template <class T> void

C++ supports template declarations which express exactly what we want: template <class T> void swap(T &x, T &y) { T temp = x; x = y; y = temp; } This declares a template function swap which can swap any type. A template is sort of like a function that takes a type as a parameter. The “class T” declaration indicates that T is an argument which can be instantiated with any type (despite the name “class”, this type can be any type, not just a class type).

template <class T> void swap(T &x, T &y) { T temp = x; x

template <class T> void swap(T &x, T &y) { T temp = x; x = y; y = temp; } swap can be instantiated with different types to produce swap functions specialized to particular types: swap<int> is a function that swaps ints. swap<string> is a function that swaps strings. For example, swap<int> substitutes int for T to produce a function to swap ints: void swap(int &x, int &y) { int temp = x; x = y; y = temp; }

template <class T> void swap(T &x, T &y) { T temp = x; x

template <class T> void swap(T &x, T &y) { T temp = x; x = y; y = temp; } Here’s an example that uses swap<int> to swap a couple of integers, and swap<float> to swap a couple of floating point numbers: int i = 3, j = 4; swap<int>(i, j); float f = 4. 0, g = 5. 0; swap<float>(f, g);

template <class T> void swap(T &x, T &y) { T temp = x; x

template <class T> void swap(T &x, T &y) { T temp = x; x = y; y = temp; } In practice, C++ can usually infer the right type parameter based on the arguments to a template function: int i = swap(i, float f swap(f, 3, j = j); // = 4. 0, g); // 4; uses swap<int> g = 5. 0; uses swap<float>

To see how useful templates can be, recall the qsort function in stdlib. h:

To see how useful templates can be, recall the qsort function in stdlib. h: void qsort(void *base, int num, size_t width, int (*compare)(void *elem 1, void *elem 2 ) ); This was messy to use because of void*. For instance, our compare function for integers looked like: int compare. Int(void *i 1 Ptr, void *i 2 Ptr) { if(*((int*)i 1 Ptr) < *((int*)i 2 Ptr)) return -1; else if(*((int*)i 1 Ptr) == *((int*)i 2 Ptr)) return 0; else return 1; }

We can use templates to get rid of the void*: template <class T> void

We can use templates to get rid of the void*: template <class T> void qsort(T *base, int num, int (*compare)(T& elem 1, T& elem 2 ) ); This declares a template function qsort which can sort any type.

template <class T> void qsort(T *base, int num, int (*compare)(T& elem 1, T& elem

template <class T> void qsort(T *base, int num, int (*compare)(T& elem 1, T& elem 2 ) ); Example (notice that we can write a nice compare. Int function without a bunch of casts): int compare. Int(int& i 1, int& i 2) { if(i 1 < i 2) return -1; else if(i 1 == i 2) return 0; else return 1; } int i. Arr[4]; i. Arr[0] = 3; i. Arr[1] = 2; i. Arr[2] = 7; i. Arr[3] = 5; qsort<int>(i. Arr, 4, compare. Int);

Notice also that the templates enforce a stronger typing property than the void* did:

Notice also that the templates enforce a stronger typing property than the void* did: the type of the compare function passed in must match the type of the array. So qsort(i. Arr, 4, compare. Int); typechecks, because compare. Int and i. Arr both are built out of type int. But string s. Arr[4]; qsort(s. Arr, 4, compare. Int); won’t typecheck, because s. Arr doesn’t match the type of the compare. Int function.

Templates can also be used to create generic data structures. Consider our old Float.

Templates can also be used to create generic data structures. Consider our old Float. Array class, which only worked for elements of type float: class Float. Array { float *arr; int arr. Size; public: Float. Array(int size); Float. Array(const Float. Array& from); Float. Array& operator = (const Float. Array& from); ~Float. Array(); float get(int i) const; void set(int i, float f); int size(); };

By declaring and implementing the class as a template, we can make it work

By declaring and implementing the class as a template, we can make it work for any type: template <class T> class TArray { T *arr; int arr. Size; public: TArray(int size); TArray(const TArray<T>& from); TArray<T>& operator = (const TArray<T>& from); ~TArray(); T get(int i) const; void set(int i, T f); int size(); }; For example, TArray<float> is a class for an array of floats, and TArray<string> is a class for an array of strings.

template <class T> class TArray { T *arr; int arr. Size; public: TArray(int size);

template <class T> class TArray { T *arr; int arr. Size; public: TArray(int size); TArray(const TArray<T>& from); TArray<T>& operator = (const TArray<T>& from); ~TArray(); T get(int i) const; void set(int i, T f); int size(); }; Example: TArray<int> t. Arr(4); t. Arr. set(3, 5); int i = t. Arr. get(3);

template <class T> class TArray { T *arr; int arr. Size; public: TArray(int size);

template <class T> class TArray { T *arr; int arr. Size; public: TArray(int size); TArray(const TArray<T>& from); TArray<T>& operator = (const TArray<T>& from); ~TArray(); T get(int i) const; void set(int i, T f); int size(); }; Implementing member functions of a template class: template <class T> int TArray<T>: : size() { return arr. Size; }

Template functions and template classes can be used together. For instance, the following function

Template functions and template classes can be used together. For instance, the following function sorts a TArray: template <class T> void arraysort(TArray<T> &arr, int (*compare)(T& elem 1, T& elem 2 ) ); Notice again that the element type of the TArray must match the type used by the compare function. Example: TArray<int> t. Arr(4); . . . arraysort<int>(t. Arr, compare. Int);

A template can take multiple type arguments: template <class T, class U> class Pair

A template can take multiple type arguments: template <class T, class U> class Pair { public: T x 1; U x 2; Pair(T a 1, U a 2): x 1(a 1), x 2(a 2) {} }; Pair<string, int> p 1("deep ", 6); cout << p 1. x 1 << p 1. x 2 << endl;

The standard template library There a number of built-in data structures that are available

The standard template library There a number of built-in data structures that are available for you to use, such as list and vector. For instance, list<string> is a list of strings, and vector<Shape*> is a vector of pointers to Shapes. Example: #include <list> #include <vector> using namespace std; // necessary on some platforms list<int> l 1; // list of ints vector<Shape*> v 1; // vector of pointers to Shapes

2 dimensional arrays can now be implemented as vectors of vectors: vector<double> > arr;

2 dimensional arrays can now be implemented as vectors of vectors: vector<double> > arr; Pitfall: the space in “> >” is necessary. If you type: vector<double>> arr; then C++ thinks the >> means “right-shift”.

Polymorphism strikes again Remember how inheritance and virtual functions led to a form of

Polymorphism strikes again Remember how inheritance and virtual functions led to a form of polymorphism? A variable of type Shape could hold many different types of objects at run-time (Circles, Triangles, etc. ). Templates also provide a form of polymorphism. Inside a template<class T> function or class, a variable of type T may refer to many different types of objects, depending on what type T is instantiated with.

Let’s see how these two forms of polymorphism compare. Before templates were introduced into

Let’s see how these two forms of polymorphism compare. Before templates were introduced into C++, many vendors provided data structure classes based on inheritance. Usually, all elements would have to be derived from a common base class called Object. Then the data structure classes would work with Objects: class Array { Object **arr; public: . . . Object* get(int i) const; void set(int i, Object* f); };

class Array { Object **arr; public: . . . Object* get(int i) const; void

class Array { Object **arr; public: . . . Object* get(int i) const; void set(int i, Object* f); }; So if we write a class that inherits from Object, we can put objects of this class into an Array: class My. String: public Object {. . . }; Array arr(5); arr. set(0, new My. String(“hello”)); arr. set(1, new My. String(“there”));

class Array { Object **arr; public: . . . Object* get(int i) const; void

class Array { Object **arr; public: . . . Object* get(int i) const; void set(int i, Object* f); }; But if we try to read a My. String object from the array, we find it has type Object* instead of My. String*: Object *o = arr. get(0); So we have to do a cast (yuck!) to make it a My. String*: My. String *m = (My. String *) o;

With templates, we know that the type we get out of a data structure

With templates, we know that the type we get out of a data structure is exactly the type we put into the data structure. So no cast is necessary: template <class T> class TArray { T **arr; public: . . . T* get(int i) const; void set(int i, T* f); }; Array<My. String> arr(5); arr. set(0, new My. String(“hello”)); arr. set(1, new My. String(“there”)); My. String *m = arr. get(0); // no cast necessary So templates are clearly a nicer, safer way to implement generic data structure classes.

On the other hand, inheritance is the best way to express a common interface

On the other hand, inheritance is the best way to express a common interface shared by a collection of closely related classes. For instance, a collection of shapes can rely on a common base class Shape: class Shape. . . public: virtual }; { void draw(); void rotate(double angle); double area(); class Circle: public Shape {. . . }; class Triangle: public Shape {. . . };

Suppose we didn’t use inheritance to implement shapes, and instead defined each shape independently,

Suppose we didn’t use inheritance to implement shapes, and instead defined each shape independently, without a common base class: class XCircle {. . . void draw(); void rotate(double angle); double area(); }; class XTriangle {. . . void draw(); void rotate(double angle); double area(); };

Now suppose we wrote a template function rotate. And. Draw, designed to work for

Now suppose we wrote a template function rotate. And. Draw, designed to work for XCircles and XTriangles: template<class T> void rotate. And. Draw(T &s, double angle) { s. rotate(angle); s. draw(); } The function assumes that T contains rotate and draw member functions, but it doesn’t document this clearly. For instance, it doesn’t say what argument and return types the rotate and draw functions should have. This is bad style.

It isn’t clear from the template declaration template<class T> void rotate. And. Draw(T &s,

It isn’t clear from the template declaration template<class T> void rotate. And. Draw(T &s, double angle); what types are valid for T. For instance, is an int ok? rotate. And. Draw<int>(5, 3. 0); // ? ? ? This in fact does not typecheck, because ints don’t have rotate and draw functions. However, we have to look at rotate. And. Draw’s implementation to figure this out. Using inheritance is much better in this case: void rotate. And. Draw(Shape &s, double angle) { s. rotate(angle); s. draw(); } Here, the type of s is clearly stated.

Inheritance also allows more heterogeneity. Suppose we want a single list that holds all

Inheritance also allows more heterogeneity. Suppose we want a single list that holds all different kinds of shapes. If shapes aren’t related by inheritance, we can create lists of specific types: list<XCircle*> l 1; list<XTriangle*> l 2; But l 1 can only hold XCircles, and l 2 can only hold XTriangles. Neither list can hold a mixture of the two. If Circle and Triangle share the base class Shape, though, then list<Shape*> l 3; can hold all different kinds of shapes, including Circles and Triangles.

Notice that list<Shape*> l 3; takes advantage both of templates and inheritance. This combination

Notice that list<Shape*> l 3; takes advantage both of templates and inheritance. This combination of templates and inheritance is quite common and very useful. It uses the strengths of templates and inheritance in a complementary way.

Templates: the good, the bad, the ugly good: • templates are excellent at expressing

Templates: the good, the bad, the ugly good: • templates are excellent at expressing generic algorithms and data structures bad and ugly: • template implementations go into header files instead of. cpp files • template instantiations create multiple copies of a template’s code • template uses can’t be typechecked without expanding the entire template

Problem #1: On most platforms, the entire implementation of a template must go in

Problem #1: On most platforms, the entire implementation of a template must go in the header file: // swap. h: template <class T> void swap(T &x, T &y) { T temp = x; x = y; y = temp; } //. cpp file: #include “swap. h”. . . This violates everything we’ve told you about source code organization. Usually only declarations go in the header file, and implementations go in a. cpp file.

Problem #2: A different copy of a template’s code is produced for every different

Problem #2: A different copy of a template’s code is produced for every different type passed in as the templates type parameter. template <class T> void qsort(T *base, int num, int (*compare)(T& elem 1, T& elem 2 ) ); For instance, if the qsort template above is instantiated for ints, floats, and strings, then 3 different copies of qsort’s code are generated: qsort<int>(. . . ); // generates code for qsort<int> qsort<float>(. . . ); // generates code for qsort<float> qsort<string>(. . . ); // generates code for qsort<string>

If the templates are complicated, then this “code bloat” can get quickly out of

If the templates are complicated, then this “code bloat” can get quickly out of hand. C++ programmers sometimes complain that their executable files are many megabytes in size because of template instantiation. By contrast, the old qsort in stdlib. h only required one copy of the code, which worked for all types (through the admittedly clunky void* mechanism).

Problem #3: typechecking template instantiations template<class T> void rotate. And. Draw(T &s, double angle);

Problem #3: typechecking template instantiations template<class T> void rotate. And. Draw(T &s, double angle); We’ve already seen the rotate. And. Draw example that expects its type T to have rotate and draw member functions. This means that trying to use rotate. And. Draw<int>(5, 3. 0); won’t typecheck. But how do we know that it won’t typecheck? We have to look through rotate. And. Draw’s implementation, to see what things it does with the objects of type T. This violates the idea of separating the implementation of a function from its interface.

A practical consequence of this is that C++ needs to expand out uses of

A practical consequence of this is that C++ needs to expand out uses of a template entirely before it can typecheck them. If the template is complicated, This leads to remarkably obscure type errors when something goes wrong. Here’s a small (incorrect) example that uses the standard library class map: map<string, int> m 1; // maps strings to ints // incorrectly tries to insert a pair (“hello”, 6) // into the map: m 1. insert(m 1. begin(), make_pair("hello", 6)); This incorrectly passes in a char* argument (“hello”) to make_pair instead of a string. Let’s see what type error we get.

map<string, int> m 1; // maps strings to ints m 1. insert(m 1. begin(),

map<string, int> m 1; // maps strings to ints m 1. insert(m 1. begin(), make_pair("hello", 6)); Visual Studio 5. 0 reports the following type error: error C 2664: 'class std: : _Tree<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, struct std: : pair<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int>, struct std: : map<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int, struct std: : less<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>>, class std: : allocator<int>>: : _Kfn, struct std: : less<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>>, class std: : allocator<int>>: : iterator std: : map<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int, struct std: : less<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>>, class std: : allocator<int>>: : insert(class std: : _Tree<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, struct std: : pair<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int>, struct std: : map<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int, struct std: : less<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>>, class std: : allocator<int>>: : _Kfn, struct std: : less<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>>, class std: : allocator<int>>: : iterator, const struct std: : pair<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int> &)' : cannot convert parameter 2 from 'struct std: : pair<char [6], int>' to 'const struct std: : pair<class std: : basic_string<char, struct std: : char_traits<char>, class std: : allocator<char>>, int> &'

Don’t be scared off too much by the problems with C++ templates. Templates are

Don’t be scared off too much by the problems with C++ templates. Templates are still a good idea! But we need to remember that templates are best used for small, simple container classes. Large and complicated templates (such as the map example above) often lead to code bloat and difficult error messages. Simple classes, like list and vector, tend to work very well in practice. We’ll explore these standard library classes in the next lecture.