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.
We can also declare references to constant variables as follows:
This is pretty reasonable and behaves just as you’d expect. But beware, you cannot bind a non-const reference to a const variable:
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:
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:
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
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.
Ahas parametersdouble, int, int, which is worse thanint, int, intin the first coordinate and equal in the last two.Bhas parametersdouble, double, int, which is an even worse match thanA.Dhas parametersint, double, int, which likeAis 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!