Session 4: Genericity, Containers

Polymorphism

Code that works for many types.

See also:

A problem of reuse

Swapping arguments

Swapping a pair of integers:
        void swap(int & x, int & y) {
                int tmp = x; x = y; y = tmp;
        }
Swapping a pair of strings is very similar:
        void swap(string & x, string & y) {
                string tmp = x; x = y; y = tmp;
        }
And so on for every other type.

Idea: make the type a parameter, and instantiate it to int, string or any other type.

A generic swapping procedure

Instead of the preceding versions, we can write:
        template <typename T>
        void swap(T & x, T & y) {
                T tmp = x; x = y; y = tmp;
        }
Here T is a type parameter. When we use this function, T is instantiated to the required type:
        int i, j;
        swap(i, j);    // T is int
        string s, t;
        swap(s, t);    // T is string
but in each use T must stand for a single type.

Writing generic code

Reversing a vector of integers

        void reverse(vector<int> & v) {
                int l = 0;
                int r = v.size()-1;
                while (l < r) {
                        swap(v[l], v[r]);
                        l++;
                        r--;
                }
        }
Reversing a vector of strings is the same, except for string instead of int as the element type.

A generic reversal procedure

Instead of the preceding versions, we can write:
        template <typename Elem>
        void reverse(vector<Elem> & v) {
                int l = 0;
                int r = v.size()-1;
                while (l < r) {
                        swap(v[l], v[r]);
                        l++;
                        r--;
                }
        }
Possible strategy: write a specific version and then generalize.

Using the generic procedure

We can call reverse with vectors of any type, and get a special version for that type:
        vector<int> vi;
        vector<string> vs;
        ...
        reverse(vi);         // T = int
        reverse(vs);         // T = string
This works for any type:
        vector<vector<int> > vvi;
        ...
        reverse(vvi);        // T = vector<int>

Implementation methods

Code sharing:
a single instance of the generic code is generated, and shared between all uses. This requires a common representation for types, and is often used in functional languages.

Instantiation (or specialization):
an instance of the code is generated for each specific type given as an argument, possibly avoiding unused instances (C++).

Caution: these methods are only instantiated (and fully checked) when used.

Another example

Testing whether a value occurs in a vector:
        template <typename Elem>
        bool member(Elem x, const vector<Elem> & v) {
                for (int i = 0; i < v.size(); i++)
                        if (v[i] == x)
                                return true;
                return false;
        }
The generic definition of member only makes sense if the operator == is defined for T.

Bounded genericity

A generic class

The following class is defined in <utility>:
    template <typename A, typename B>
    class pair {
    public:
        A first;
        B second;
        pair(const A& a, const B& b) :
            first(a), second(b) {}
    };
Some pair objects:
        pair<int, int> p(3, 4);
        pair<int, string> n(12, "twelve");
Note that we must specify the type arguments.

Container classes in the STL

The Standard Template Library is part of the C++ standard library, and provides several template classes, including

The vector class

template <typename T>
class vector {
public:
    vector();
    vector(int initial_size);

    int size() const;
    void clear();

    T & operator[](int index) const;
    T & front() const { return operator[](0); }
    T & back() const { return operator[](size() - 1); }
    void push_back(const T & x);
    void pop_back();
};

Another container: lists

The list class

    template <typename T> class list {
    public:
        list();

        int size() const;
        void clear();

        T & front() const;
        void push_front(const T & x);
        void pop_front();
        T & back() const;
        void push_back(const T & x);
        void pop_back();
    };

Using a list

Reversing the order of the input lines:
        list<string> stack;
        string s;
        while (getline(cin, s))
                stack.push_back(s);
        while (stack.size() > 0) {
                cout << stack.back() << '\n';
                stack.pop_back();
        }

Commonality between STL containers

Requirements on containers in the STL

But Container, Sequence etc are not C++: they do not appear in programs, and so cannot be checked by compilers.

Some STL terminology

The STL documentation uses the following terms: Remember that all this is outside the C++ language.

New template classes from old

Often template classes are built using existing template classes. The following is defined in <stack>:
template <typename Item>
class stack {
    vector<Item> v;
public:
    bool empty() const { return v.size() == 0; }
    void push(const Item & x) { v.push_back(x); }
    Item & top() { return v.back(); }
    void pop() { v.pop_back(); }
};

Defining methods outside the class

As with ordinary classes, we can defer the definition of methods:
    template <typename Item>
    class stack { 
        vector<Item> v;
    public:
        Item & top();
        ...
    };
The method definition must then be qualified with the class name, including parameter(s):
    template <typename Item>
    Item & stack<Item>::top() { return v.back(); }

Maps

A map is used like an vector, but may be indexed by any type:
    map<string, int> days;
    days["January"] = 31;
    days["February"] = 28;
    days["March"] = 31;
    ...
    string m;
    cout m << " has " << days[m] << " days\n";
    cout << "There are " << days.size() << " months\n";
This is a mapping from strings to integers.

The map class

    template <typename Key, typename Value>
    class map {
        map();
    
        int size() const;
        void clear();
    
        int count(Key k);   // 0 or 1
        Value & operator[](Key k);
    };
The expression m[k] creates an entry for k if none exists.

Summary

Next week