C++ Boot Camp - Advanced Topics

Written by: Milad Fatenejad

There are a few advanced topics that we won't have time to cover in the boot camp. However, we wanted to make sure that we included them in the notes. So this section will be devoted to a discussion of some of these advanced topics. These include:

  • Operator overloading
  • Templates and generic programming
  • Exceptions
  • Casting

A large part of this section will involve creating a simple two dimensional array class. While making this class, we will review some old topics as well. Lets start by making a really simple version of our class. For simplicity, the entire class will be held in a single header file and all of the functions will be inlined. The simple version of our class, Arr2d, is shown below:

array-simple.hpp

Line 
1#ifndef __ARR2D_SIMPLE_HPP__
2#define __ARR2D_SIMPLE_HPP__
3
4#include <vector>
5
6class Arr2d 
7{
8public:
9
10  /// This is the default constructor. It doesn't do anything:
11  Arr2d() :
12    nrows(0), ncols(0)
13  {
14  }
15
16  /// Create a 2D array that is nr by nc.
17  Arr2d(int nr, int nc)
18  {
19    resize(nr, nc);
20  }
21
22  /// Create a 2D array that is given a default value:
23  Arr2d(int nr, int nc, double val)
24  {
25    resize(nr, nc);
26    setall(val);
27  }
28
29  /// Resize the array:
30  void resize(int nr, int nc)
31  {
32    nrows = nr;
33    ncols = nc;
34
35    storage.resize(nrows);
36    for(int i = 0; i < nrows; i++) {
37      storage[i].resize(ncols);
38    }   
39  }
40
41  /// Set all of the array elements to some value:
42  void setall(double val)
43  {
44    int i, j;
45    for(i = 0; i < nrows; i++) {
46      for(j = 0; j < ncols; j++) {
47        storage[i][j] = val;
48      }
49    }
50  }
51
52  /// Get an element of the array. By returning a reference, we can
53  /// use this function to change the value:
54  double& get(int r, int c)
55  {
56    return storage[r][c];
57  }
58
59  /// Get an elemnt of the array. This can be used for const arrays
60  const double& get(int r, int c) const
61  {
62    return storage[r][c];
63  }
64
65private:
66
67  /// The storage member stores all of our data:
68  std::vector<std::vector<double> > storage;
69
70  /// The number of rows:
71  int nrows;
72
73  /// The number of columns:
74  int ncols;
75
76};  // <-- Make sure that you include this semicolon
77
78#endif

First, notice that like all header files, we begin and end the file with the preprocessor directives:

#ifndef __ARR2D_SIMPLE_HPP__
#define __ARR2D_SIMPLE_HPP__
...
#endif

This ensures that errors related to including files twice do not occur. Again, ALL header files should have lines like this at the beginning and end.

This file includes the definition of the class Arr2d which is a simple 2D array class (or matrix class). The actual data for the array is stored as a vector of vectors in the member variable storage - defined on line 68. This version of our array class stores arrays of double precision numbers. We also store the number of rows and columns in the member variables nrows and ncols defined on lines 71 and 74, respectively. There are three constructors for this class that allow us to create our 2D array in a few different ways. The constructor on line 11 takes no arguments - it is the default constructor. It creates a 2D array which has zero rows and zero columns. This constructor is not entirely useless, because we can resize our array using the resize the member function on line 30. The second constructor, whose definition begins on line 17, allows us to define an array which is given an initial size. Notice that it simply calls the resize function internally. The final constructor allows us to set the initial size of the array and set the initial value of all of the elements. Notice, again, that internally it calls other public member functions.

But how do users access the data in the array? The two member functions, called get, allow you to access the array elements. Why are there two functions? One is used when a const Arr2d is created and the other is used when a non-const Arr2d is created. The following file shows how the Arr2d class can be used:

simple.cpp

Line 
1#include <iostream>
2#include "arr2d-simple.hpp"
3
4int main()
5{
6  // Create a 2D array object with 5 rows and 4 columns. Initialize
7  // the values to 1.0:
8  Arr2d arr2d(5,4, 1.0);
9
10  // Set some elements of the array:
11  arr2d.get(3,3) = 2.0;
12
13  // Print the array:
14  for(int i = 0; i < 5; i++){
15    for(int j = 0; j < 4; j++) {
16      std::cout << arr2d.get(i,j) << '\t';
17    }
18    std::cout << std::endl;
19  }
20
21  return 0;
22}

If line 8 had created a const Arr2d instead of an Arr2d, line 11 would cause an error because we are trying to change the array, but the rest of the program would still work.

Templates and Exceptions

Our 2D array class is good - but not great. There are a few problems with it. First, it doesn't check for errors. Second, it only allows us to create arrays of double precision numbers. We've fixed these problems in the improved version of this class, shown below:

array-template.hpp

Line 
1#ifndef __ARR2D_TEMPLATE_HPP__
2#define __ARR2D_TEMPLATE_HPP__
3
4#include <vector>
5#include <stdexcept>
6
7template <class T>
8class Arr2d 
9{
10public:
11
12  /// This is the default constructor. It doesn't do anything:
13  Arr2d() :
14    nrows(0), ncols(0)
15  {
16  }
17
18  /// Create a 2D array that is nr by nc.
19  Arr2d(int nr, int nc)
20  {
21    resize(nr, nc);
22  }
23
24  /// Create a 2D array that is given a default value:
25  Arr2d(int nr, int nc, T val)
26  {
27    resize(nr, nc);
28    setall(val);
29  }
30
31  /// Resize the array:
32  void resize(int nr, int nc)
33  {
34    // Make sure that the number of rows is non-negative:
35    if(nr < 0) {
36      throw(std::runtime_error("Number of rows cannot be negative"));
37    }
38
39    if(nc < 0) {
40      throw(std::runtime_error("Number of columns cannot be negative"));
41    }
42
43    nrows = nr;
44    ncols = nc;
45
46    storage.resize(nrows);
47    for(int i = 0; i < nrows; i++) {
48      storage[i].resize(ncols);
49    }   
50  }
51
52  /// Set all of the array elements to some value:
53  void setall(T val)
54  {
55    int i, j;
56    for(i = 0; i < nrows; i++) {
57      for(j = 0; j < ncols; j++) {
58        storage[i][j] = val;
59      }
60    }
61  }
62
63  /// Get an element of the array. By returning a reference, we can
64  /// use this function to change the value:
65  T& get(int r, int c)
66  {
67    return storage[r][c];
68  }
69
70  /// Get an elemnt of the array. This can be used for const arrays
71  const T& get(int r, int c) const
72  {
73    return storage[r][c];
74  }
75
76private:
77
78  /// The storage member stores all of our data:
79  std::vector<std::vector<T> > storage;
80
81  /// The number of rows:
82  int nrows;
83
84  /// The number of columns:
85  int ncols;
86
87};  // <-- Make sure that you include this semicolon
88
89#endif

In this example, we have told the compiler that we want our Arr2d class to hold variables of any generic type T, rather than simply double. This is something called template or generic programming. We tell the compiler that Arr2d is supposed to use a generic type T on line 7. We have then simply gone through the program and replaced every double with a T. With this small change, we can now make an Arr2d which contains any type. Take a look at the following example:

template.cpp

Line 
1#include <iostream>
2#include "arr2d-template.hpp"
3
4int main()
5{
6  int nr, nc;
7
8  // Create a 2D array object with 5 rows and 4 columns. Initialize
9  // the values to 1.0:
10  Arr2d<std::string> arr2d(5,4, "Hello, World!");
11
12  // Print the array:
13  for(int i = 0; i < 5; i++){
14    for(int j = 0; j < 4; j++) {
15      std::cout << arr2d.get(i,j) << '\t';
16    }
17    std::cout << std::endl;
18  }
19
20  // Try to create a bad array:
21  Arr2d<int> bad_array(3,-1, 0);
22
23  return 0;
24}

We create an Arr2d variable on line 10. Now, we must specify the type of information we want our array to hold in angle brackets. In this case, we want to create a 2D array of strings. Notice that the syntax for creating arrays is a lot like the syntax for create vectors. That is because they both are template classes. Line 10 instructs the compiler to create a 5x4 array where each element is initialized to the string, "Hello, World!". When creating a templated class, all of the definitions of the member functions must be declared in the header file. To understand why, you have to think about what the compiler does when it gets to line 10. Line 10 tells the compiler that it needs to make a variable that is of type 'Arr2d<std::string>', which is a completely different type than a variable of type 'Arr2d<int>' or 'Arr2d<double>'. Furthermore, when it gets to line 10, all the compiler has to work with is a generic type, Arr2d<T> (it knows this because we included the header file arr2d-template.hpp in template.hpp). When it sees that a variable of type 'Arr2d<std::string>' it creates the type using Arr2d<T>. In order to create the new type Arr2d<std::string> from Arr2d<T>, the compiler needs to know everything about that type, including exactly what the functions do. That is why the entire type - including function definitions - must be included in the header file.

The other interesting line in this program is line 21. This line tries to create an array that has -1 columns. This is probably a typo, and it would be create if the program said something when it ran. If you have used Java or python, you are familiar with exceptions. On lines 35 through 41 of arr2d-template.hpp we check to make sure that the number of rows and columns is not negative. If a negative row number is encountered, an exception is thrown. For now, think of an exception as an error message. An exception causes the program to immediately exit each function until the exception is caught somewhere. If the exception reaches the main program and is still not caught, the program terminates. Lets look at the output from the above program:

Hello, World!	Hello, World!	Hello, World!	Hello, World!	
Hello, World!	Hello, World!	Hello, World!	Hello, World!	
Hello, World!	Hello, World!	Hello, World!	Hello, World!	
Hello, World!	Hello, World!	Hello, World!	Hello, World!	
Hello, World!	Hello, World!	Hello, World!	Hello, World!	
terminate called after throwing an instance of 'std::runtime_error'
  what():  Number of columns cannot be negative
Aborted

The last 3 lines are caused by the fact that we threw an exception in the function Arr2d::resize on line 40. That exception causes the program to exit that function immediately, so execution jumps to line 27. But, the exception is not caught there either, so execution jumps to line 21 of template.cpp. The exception is not caught there either, so the program exits, and an error message is printed. So, how do we catch exceptions in C++? Well, we could have replaced line 21 of template.cpp by the following lines:

try {
  Arr2d<int> bad_array(3,-1, 0);
} catch ( std::runtime_error& ex ) {
  std::cout << "Error, I made a mistake when I created the array.\n";
}

The keyword, try, tells the compiler that the block of code between the curly braces might throw an exception. We have thrown an exception of type std::runtime_error on line 40 of arr-template.hpp. On the fourth line above, we catch exceptions of this type. If no exception is thrown, everything goes normally and the catch block is not executed. Since an exception is thrown, the string "Error, I made a mistake when I created the array.\n" is printed. There is a lot more to C++ exceptions than what is described here, but I hope this gives you some introduction to the topic.

Operator Overloading

Now, our two dimensional array program is a little better. We can make arrays of any type, and we check for simple errors using exceptions. But lets make this class great by overloading some operators. Right now we have to access array elements using code such as:

Arr2d<double> arr2d(5,5); // Create a 5x5 array of doubles
arr2d.get(3,4) = 0.5;     // Change element 3, 4 of the array using the get member function

Wouldn't it be great if we didn't have to use the function get? For example, it would be nice if we could do the following:

Arr2d<double> arr2d(5,5); // Create a 5x5 array of doubles
arr2d(3,4) = 0.5;     // Change element 3, 4 of the array using the parentheses operator

Notice that we didn't use the get function, we just use parentheses. By making a trivial change to our Arr2d class, we can make the compiler accept the syntax above. All we have to do is change the function name, get, to the function name operator(). We have done this in the example below:

arr2d-operators.hpp

Line 
1#ifndef __ARR2D_OPERATORS_HPP__
2#define __ARR2D_OPERATORS_HPP__
3
4#include <vector>
5#include <stdexcept>
6
7template <class T>
8class Arr2d 
9{
10public:
11
12  /// This is the default constructor. It doesn't do anything:
13  Arr2d() :
14    nrows(0), ncols(0)
15  {
16  }
17
18  /// Create a 2D array that is nr by nc.
19  Arr2d(int nr, int nc)
20  {
21    resize(nr, nc);
22  }
23
24  /// Create a 2D array that is given a default value:
25  Arr2d(int nr, int nc, T val)
26  {
27    resize(nr, nc);
28    setall(val);
29  }
30
31  /// Resize the array:
32  void resize(int nr, int nc)
33  {
34    // Make sure that the number of rows is non-negative:
35    if(nr < 0) {
36      throw(std::runtime_error("Number of rows cannot be negative"));
37    }
38
39    if(nc < 0) {
40      throw(std::runtime_error("Number of columns cannot be negative"));
41    }
42
43    nrows = nr;
44    ncols = nc;
45
46    storage.resize(nrows);
47    for(int i = 0; i < nrows; i++) {
48      storage[i].resize(ncols);
49    }   
50  }
51
52  /// Set all of the array elements to some value:
53  void setall(T val)
54  {
55    int i, j;
56    for(i = 0; i < nrows; i++) {
57      for(j = 0; j < ncols; j++) {
58        storage[i][j] = val;
59      }
60    }
61  }
62
63  /// *****************
64  /// ***           ***
65  /// *** OPERATORS ***
66  /// ***           ***
67  /// *****************
68
69  /// We've replaced our get functions with overloaded () operators:
70
71
72  /// Get an element of the array. By returning a reference, we can
73  /// use this function to change the value:
74  T& operator()(int r, int c)
75  {
76    return storage[r][c];
77  }
78
79  /// Get an elemnt of the array. This can be used for const arrays
80  const T& operator()(int r, int c) const
81  {
82    return storage[r][c];
83  } 
84
85private:
86
87  /// The storage member stores all of our data:
88  std::vector<std::vector<T> > storage;
89
90  /// The number of rows:
91  int nrows;
92
93  /// The number of columns:
94  int ncols;
95
96};  // <-- Make sure that you include this semicolon
97
98
99/// Overload the insertion stream operator so we can easily print the
100/// array. Note that this function is not implemented as a member
101/// function.
102template <class T>
103std::ostream& operator<<(std::ostream& os, const Arr2d<T>& arr2d)
104{
105  int i, j;
106 
107  // Print the array:
108  for(i = 0; i < 5; i++){
109    for(j = 0; j < 4; j++) {
110      os << arr2d(i,j) << '\t';
111    }
112    os << std::endl;
113  }
114 
115  return os;
116}
117
118#endif

Notice that in this iteration of our simple 2d array class, we have replaced the get functions with the operator() functions. So what happened here? The way to understand what we did is, as always, think about what the compiler does we write something like:

arr2d(4,3) = 2.0;

When the compiler gets to this line, it replaces it with something like:

arr2d.operator()(4,3) = 2.0;

It then checks to see if you have defined a function called operator(). If you have, then everything runs fine. If you haven't - the compiler freaks out. We've talked a lot about operators throughout this tutorial without defining exactly what we mean, or more importantly, how the compiler handles operators. In C++, operators are simply functions that have funny names. When the compiler gets to any operator, it replaces it with a function call. We saw how this happened above for the parentheses operator, but it also happens for things like addition and subtraction. For example, when you add two numbers, like so:

double a, b, c;
a = b + c;

The compiler replaces the second line with:

a = operator+(b,c);

If for our class, Arr2d, we wrote a function called operator+, then we could add two arrays together. There are all sorts of operators we can overload - and this capability gives C++ classes extreme flexibility. For example, in all of the previous programs we have looped over our array and printed each element. Wouldn't it be great if we could just print our array all at once? We can if we overload the insertion stream operator, <<, as is done on lines 102 to 116 of the file arr2d-operators.hpp. Notice that this operator is declared outside of the class definition. It is also an example of a templated function (so far, we've only seen a templated class, but the idea is similar). With the enhancements we've made, we can now have the following source code work:

operators.cpp

Line 
1#include <iostream>
2#include "arr2d-operators.hpp"
3
4int main()
5{
6  int nr, nc;
7
8  // Create a 2D array object with 5 rows and 4 columns. Initialize
9  // the values to 1.0:
10  Arr2d<std::string> arr2d(5,4, "Hello, World!");
11
12  // Now we can access the array elements using the parentheses operator:
13  arr2d(1,3) = "Goodbye, World!";
14 
15  // We've overloaded the insertion stream operator, so we can print
16  // things really easily now:
17  std::cout << arr2d;
18
19  return 0;
20}

By using templates, exceptions, and operator overloading, we have created an extremely robust class that was relatively painless to write.

Goodbye

This is the end of the C++ tutorial for the C++ boot camp. I hope you have learned some useful things. The syntax of C++ can be a little overwhelming at times, but if you keep in mind how the compiler works, it can actually make a lot of sense. Thanks for reading - Milad and Kyle.