Hunter Liu's Website

13. Week 7 Thursday: const References and Overloading

≪ 12. Week 7 Tuesday: Vectors | Table of Contents | 14. Week 8: Blackjack! ≫

There are two midterm-related concepts I’d like to focus on today, each too short for one discussion but relevant enough to talk about.

const References

We should think about the keyword const as a part of the label on a box reperesenting a variable. Whenever C++ sees a label that has the word const on it, it knows not to modify the contents of that box and will complain with a build error if you try.

1const int n = 5; 
2n += 17; // build error! 

We can also declare references to constant variables as follows:

1const int n = 5; 
2const int& r = n; 

This is pretty reasonable and behaves just as you’d expect. But beware, you cannot bind a non-const reference to a const variable:

1const int n = 5; 
2int& r = n; // build error! 

Morally this makes sense: the box called n is labelled as const, and I can’t circumvent these security measures just by slapping a non-const label on the same box.

On the flip side, I can add labels with heightened security:

1int n = 5; 
2const int& r = n; 
3
4n = 17; 
5cout << r << endl; // now 17! 

The point is that the label dictates the level of access. You can add restricted labels on top of unrestricted labels, but you can’t add unrestricted labels on top of restricted labels.

This is most important when passing by reference, epsecially when constant variables are involved. Consider the following code:

 1void const_ref(const int& n) {
 2    cout << "const_ref called" << endl; 
 3} 
 4
 5void non_const_ref(int& n) {
 6    cout << "non_const_ref called" << endl; 
 7} 
 8
 9int main() {
10    int n = 5;
11    const int c = 7; 
12
13    const_ref(c); // okay 
14    const_ref(n); // okay 
15
16    non_const_ref(n); // okay 
17    non_const_ref(c); // not okay!! 
18
19    return 0;
20} 

Let’s interpret this in the warehouse model. The main program has two boxes, n and c. The main program is allowed to modify n but can’t touch c. When he calls const_ref, he is passing an integer box to a servant, but says that under no circumstances is the servant allowed to change the contents of said box. This is okay in both calls.

When the main program calls non_const_ref(n), he is passing the servant the box n and telling the servant to do whatever he needs to do to n. This is fine; we’re authorised to modify n ourselves. But the same call to non_const_ref(c) fails: the main program isn’t allowed to modify c, yet he’s trying to pass it off to a servant who supposedly is.

This has practical ramifications when we design functions that leverage passing by reference. Consider the following function:

 1// computes the average of the entries in a vector of doubles. 
 2double average(vector<double>& vec) {
 3    if(vec.size() == 0) {
 4        return 0;
 5    } 
 6
 7    double sum = 0; 
 8    for(int i = 0; i < vec.size(); i++) {
 9        sum += vec.at(i); 
10    } 
11
12    return sum / vec.size(); 
13} 

Logically, there is nothing wrong with the function body. But suppose we have the following code in the main function:

1const vector<double> data = { 2, 3, 5, 7, 11, 13 }; 
2cout << average(data) << endl; 

There is absolutely no reason I shouldn’t be able to compute the average of a const vector. Nowhere in the average function does the vector even get modified. But because of this “elevation of privilege” issue alluded to earlier, C++ will create a build error, and we are simply not allowed to compute the average of constant vectors.

There are two options: either live with the consequences of that, or adjust the function to

1double average(const vector<double>& vec) {
2    // ... 
3} 

If there’s one thing you take away from this section, let it be this: When you are designing functions that are passing by reference, you need to tell C++ explicitly whether or not your function actually needs to change the references it’s receiving.

Function Overloading

One great feature of language is that we can use the same word to mean many different things, and C++ is no different. Consider that the command cin >> n; behaves differently depending on what type the variable n is, for instance.

We can do this in C++ as well by providing multiple definitions of the same function with different parameters.

Let’s go back to our average example. Perhaps I want to compute the average of both vectors of doubles and vectors of ints. Although individual ints can be converted to doubles, C++ doesn’t have a way of converting the vectors. The fix is to define two different versions of average as follows:

1double average(const vector<int>& vec) {
2    // ... 
3} 
4
5double average(const vector<double>& vec) {
6    // ... 
7} 

Remark 1.

You may recognise that the code is really redundant; the only thing that changed was the type. This pattern is quite common, and a more advanced way to handle this is to use templates; you will see this if you take PIC 10B.

Every time the average function is called, C++ infers from context which version to use.

The inference rule is pretty straightforward: pick the “best option” if it exists. Let’s demonstrate via example.

Problem 2.

Determine the build errors if they occur, or determine the output of the following code if there are none.

 1#include <iostream> 
 2
 3using namespace std; 
 4
 5void f(double d1, int i1, int i2) { 
 6    cout << 'A' << endl; 
 7} 
 8
 9void f(double d1, double d2, int i1) { 
10    cout << 'B' << endl; 
11} 
12void f(int i1, int i2, int i3) { 
13    cout << 'C' << endl; 
14} 
15
16void f(int i1, double d1, int i2) { 
17    cout << 'D' << endl; 
18} 
19
20int main() {
21    f(1, 2, 3.5); 
22    f(1.5, 2.5, 3); 
23    f('c', 15, 7.5); 
24
25    return 0; 
26} 

The selection criterion is, “one function call is better than the other if parameter-by-parameter the former is a better match.”

Solution

The call f(1, 2, 3.5) has parameters int, int, double. This is “closest” to definition C, which is int int int, but let’s verify that this is really better than the other three.

  • A has parameters double, int, int, which is worse than int, int, int in the first coordinate and equal in the last two.
  • B has parameters double, double, int, which is an even worse match than A.
  • D has parameters int, double, int, which like A is worse in the middle coordinate and equal in the other two.

So the first line would output C (assuming the program builds…).

The second call f(1.5, 2.5, 3) is a call to double, double, int, which is a perfect match for definition B. We don’t need to worry about the others.

But the last call f('c', 15, 7.5) is ambiguous. This is calling char, int, double. If there’s a best call, it must have an int in the middle, so we should only compare A and C. But both double, int, int and int, int, int require a conversion of the char, and although the int looks like a more appealing option, ultimately the call is ambiguous.

Let us give some warnings about this rule: C++ does not count the number of matching parameters and use that to decide which call is better. Semantically, this makes sense: C++ cannot interpret if any parameters’ interpretations should be “more important” than the others, hence goes with the most conservative policy.

 1void f(int a, int b, int c, int d, double e) {
 2    cout << "Lots of ints" << endl; 
 3} 
 4
 5void f(double a, double b, double c, double d, int e) {
 6    cout << "Lots of doubles" << endl; 
 7} 
 8
 9int main() {
10    f(1, 2, 3, 4, 5); // ambiguous!
11    return 0; 
12} 

Counting the matching parameters, it does seem like the first definition of f is “closer”. However, the rules of C++’s interpretations say that neither is strictly better than the other! This makes a build error.

Finally, we can define overloaded functions that can never be interpreted unambiguously.

 1void f(int i) {
 2    cout << "value" << endl; 
 3} 
 4
 5void f(const int& i) {
 6    cout << "const reference" << endl; 
 7} 
 8
 9int main() {
10    return 0; 
11} 

There is no way to call the functions from main without introducing problems: f(3); and int n = 5; f(n); will both cause build errors. But the program above still builds just fine. Thus the errors with ambiguous function calls are always the function calls themselves, not the definitions!