13.5. Array 2: Flexible Arrays

Time: 00:04:01 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PowerPoint)
Review

Binary trees are complex but applicable to a wide range of problems, making their implementation with templates and inclusion in a library beneficial to application programmers. The same characteristics also apply to the template classes included in the C++ libraries introduced in the next section. In contrast, the Array class offers only modest benefits beyond fundamental or non-object arrays. However, the C++ libraries include an array class whose features elevate it far above fundamental arrays and students should contrast it to the one described next.

The Array Class

Chapter 11 introduced the Array class primarily to demonstrate overloading the index operator: operator[]. Where C++'s fundamental, non-object arrays are always zero-indexed, instances of the Array class allow client programs to specify the array's upper and lower bounds: the lowest and highest legal index values. The class's constructor allocates an appropriately sized array on the heap, and the index operator translates the client's indexes into the fundamental array index values necessary to complete the operation. The initial Array class only stored one-byte characters, limiting its usefulness. Fortunately, the operations translating Array indexes to fundamental C++ arrays are independent of the stored data, making the Array class amenable to a template implementation.

#include <stdexcept>				// (a)
using namespace std;

template <class T>
class Array
{
    private:
        int  lower;				// (b)
        int  upper;
        T*   array;				// (c)

    public:
        Array(int s, int e);
        ~Array() { delete[] array; }
        T& operator[](int index);		// (d)
        T& at(int index);			// (e)

        int get_lower() { return lower; }
        int get_upper() { return upper; }
        int get_size() { return upper - lower + 1; }
};
 
 
template <class T>
Array<T>::Array(int l, int u) : lower(l), upper(u)	// (f)
{
    if (upper < lower)
        throw invalid_argument("Upper must be >= lower");

    array = new T[upper - lower + 1]{};
}

template <class T>
T& Array<T>::operator[](int index)			// (g)
{
    return array[index - lower];
}

template <class T>
T& Array<T>::at(int index)				// (h)
{
    if (index < lower || index > upper)
        throw out_of_range("Index out of bounds");

    return array[index - lower];
}
The Array class.
  1. C++ standard exception specifications.
  2. This array's lower and upper bounds.
  3. A pointer to the data array allocated on the heap.
  4. The overloaded index operator.
  5. The at function also returns a reference to the indicated element.
  6. The constructor ensures that the arguments define appropriate array bounds, throwing an exception if they don't. The constructor allocates an array on the heap if the arguments are valid.
  7. The index operator maps the Array index to the corresponding fundamental array index, returning a reference to the indicated element. The operator does not check that index is inbounds.
  8. The at function maps the index argument to the corresponding location in array and returns a reference to the indicated element. Unlike the index operator, it verifies that index is inbounds, throwing an exception if it is not.
Three STL containers, array, vector, and deque, and the string class implement the index operator and at function as illustrated here. See invalid_argument and out_of_range for more detail about the exceptions.

The Anagram Problem Revisited

Recall that we form an anagram by rearranging the letters in one phrase to form a second. So, given two phrases, the problem is determining if one is an anagram of the other. Anagrams typically only consider letters and perhaps digits, ignoring the letter's case, spaces, and punctuation characters. Chapter 8 introduced the anagram problem, demonstrating strings, arrays, and functions. The following example takes a more direct and compact approach for brevity.

The programmed solution for the anagram problem discards non-letter characters and converts all letters to lowercase - the tolower function returns lowercase letters unmodified. The program benefits from the Array class's flexible boundaries by using the letters directly as indexes in the character-count arrays. The Chapter 8 version followed the same approach but burdened the application with the index mapping. This version reduces the application's complexity by shifting the mapping task to the Array class's operator[] or at functions, allowing the application to focus more on the problem.

#include <iostream>
#include <cstring>
#include <cctype>
#include <stdexcept>	
#include "Array.h"
using namespace std;

int main()
{
    const char* p1 = "To be or not to be: that is the question, whether "		// (a)
        "it's nobler in the mind to suffer the slings and arrows of " 
        "outrageous fortune.";
    const char* p2 = "In one of the Bard's best-thought-of tragedies, " 
        "our insistent hero, Hamlet, queries on two fronts about how "
        "life turns rotten.";

    try
    {
        Array<int> a1('a', 'z');							// (b)
        Array<int> a2('a', 'z');

        for (size_t i = 0; i < strlen(p1); i++)						// (c)
            if (isalpha(p1[i]))
                a1[tolower(p1[i])]++;
        for (size_t i = 0; i < strlen(p2); i++)
            if (isalpha(p2[i]))
                a2[tolower(p2[i])]++;

        for (int i = 'a'; i <= 'z'; i++)
            cout << (char)i << '\t' << a1[i] << '\t' << a2[i] << endl;			// (d)

        for (int i = 'a'; i <= 'z'; i++)
            if (a1[i] != a2[i])
            {
                cout << "NOT an anagram" << endl;
                exit(0);
            }

        cout << a1.at(0) << endl;		  					// (f) throws an exception
    }
    catch (invalid_argument ia)								// (f)
    {
        cerr << ia.what() << endl;
    }
    catch (out_of_range oor)
    {
        cerr << oor.what() << endl;
    }

    cout << "Valid anagram" << endl;

    return 0;
}
array2.cpp: Another anagram solution.
  1. Preprocessor concatenation joins the individual strings into one.
  2. Creates two Array objects whose lower bound is 'a' and upper bound is 'z.' The compiler automatically converts the character constants to integers.
  3. The for-loops examine the characters in the phrase arrays, p1 and p2, converting all alphabetic characters to lower case. The numeric ASCII codes index the Array objects, a1 and a2. The index operator returns a reference, used here as an l-value, so the auto-increment operator adds one to the current count.
  4. The for-loop prints each letter and its corresponding counts for the two phrases. The statement uses reference the index operator returns as an r-value.
  5. The at function and operator[] have the same mapping and indexing behaviors, but at an out-of-bounds index value and throws an exception.
  6. try and catch blocks handle exceptions when they are thrown.