13. Week 7 Tuesday: Function Overloading and Default Arguments
≪ 12. Week 6 Thursday: Vectors | Table of Contents | 14. Week 7 Thursday: Constructors, Initialisation Lists, and RAII ≫Last week, we introduced vectors and functions. Functions allow us to define “verbs” in C++, and a lot of C++ libraries provide a healthy variety of verbs to use. Examples include the push_back
and at
functions from strings and vectors, the substr
function for strings, and the sqrt
function from cmath
.
The first two examples are straightforward: provide push_back
with a character for strings, and it’ll be appended to the end of the string. Provide at
with an index, and it’ll retrieve the item at index i
.
Something more subtle happens with the substr
function: it behaves differently depending on the number of parameters you pass in. s.substr(3)
gets the substring of s
starting at index 3
and running to the end of the string, and s.substr(3, 5)
gets the substring of s
starting at index 3
containing 5
characters. That is, substr
has two different meanings, and the C++ compiler can distinguish between these two meanings based on what arguments you’ve passed in.
But now consider the cin >> variable;
“function”. This isn’t a function per se, but somewhere down below the C++ compiler has to unpack this command into a list of simple instructions.
That is to say, the three cin
statements above run completely different code from each other, and the C++ compiler can distinguish between which is which based solely upon the type of the variable i
, d
, or s
.
Contrast this with how C++ handles variables: there can only be one variable of a given name within a given scope, and C++ is unable to distinguish them based on context alone. I can’t have an integer variable and a string variable named input
, for instance.
This is an pattern built into both language and into C++. When we use verbs in everyday language (and words in general), these words often have multiple meanings depending on context. Thus, today’s discussion is centred upon how we can give multiple meanings to the same verb, in the form of function overloading (featured on your next homework) and default arguments. We’ll also see how the C++ compiler distinguishes between them.
Default Arguments
This is exactly what it sounds like: you define a C++ function that takes several parameters, but some of these are optional and come with a default value in case the user doesn’t provide one. The substr
function is a good example of this.
We can write our own (rather contrived) function with a default argument. We’ll define a function void repeat(int n, char c)
that prints out the character c
n
times. However, if the user chooses not to supply us with a character, we may use the default value of *
. In code, this looks like:
1void repeat(int n, char c = '*') {
2 for(int i = 0; i < n; i++) {
3 cout << c;
4 }
5 cout << endl;
6}
In the main function, for instance, if we type repeat(5, '#');
, the program will print #####
. However, if we instead type repeat(3);
, C++ will infer that we are repeating a *
three times and print ***
instead.
When you have multiple files and thus a forward declaration of a function with default arguments, the function declaration must include the default value. For instance, if in the file functions.hpp
one has
1void repeat(int n, char c);
while in functions.cpp
one has
the compiler will fail to understand that repeat
has a default argument for c
. This is because the header file contains the blueprint and the “grammar” of the function repeat
, and it’s what C++
refers to when it looks up what repeat(3);
is supposed to mean.
In contrast, if only the hpp
file has the default argument and the cpp
file doesn’t, C++ will chug along happily.
Warning 1.
Default arguments must be at the very end of the list of parameters. C++ will produce a compilation error if we wrote
1void repeat(char c = '*', int n);
One can have multiple default arguments, and C++ will match up the user’s arguments from left to right, only using the default arguments on the rightmost parameters. For instance, the code
1void alternate(int n, char c1 = '*', char c2 = '#') {
2 for(int i = 0; i < n; i++) {
3 cout << c1 << c2;
4 }
5 cout << endl;
6}
prints the characters c1
and c2
in an alternating fashion n
times. By default, alternate(3);
prints *#*#*#
. alternate(3, 'a', 'b');
instead prints ababab
, but alternate(3, 'a');
prints a#a#a#
.
Overloading
The preceeding section illustrates how the substr
function can have multiple meanings for different syntax through default arguments, but it doesn’t answer how cin >> variable
can have different behaviours depending on the type of the parameters.
This is done through function overloading: when you give C++ multiple versions of the same function with different parameters, C++ will choose the version with the closest match. Consider this contrived code:
1void print(int n) {
2 cout << "Integer: " << n << endl;
3}
4
5void print(string s) {
6 cout << "String: " << s << endl;
7}
8
9void print(double d) {
10 cout << "Double: " << d << endl;
11}
12
13int main() {
14 int i = 10;
15 char c = 'c';
16 double d = 0.99;
17 string s = "Hello";
18 float f = 0.5;
19
20 print(i); // Integer: 10
21 print(c); // Integer: 99
22 print(d); // Double: 0.99
23 print(s); // String: Hello
24 print(f); // Double: 0.5
25 return 0;
26}
The output is indicated in the comments. Notice that even for mismatches like a char
or a float
variable, C++ knows that it makes sense to convert the char
to an int
rather than a double
or a string
. Likewise, it also knows that a float
should be converted to a double
over the other two options.
However, be aware that these are not hard and fast rules to live by: C++ will always promote a variable type to the next-closest precision, but it will struggle to decide on how to “demote” a variable. I say “promote” because char
’s are secretly defective integers, and float
’s are just worse double
’s. The below variant of the above program fails to compile:
1void print(char c) {
2 cout << "Char: " << c << endl;
3}
4
5void print(double d) {
6 cout << "Double: " << d << endl;
7
8int main() {
9 int i = 59;
10 print(l); // demote to char? promote to double??
11
12 return 0;
13}
C++ attempts to make a type conversion to match the overloaded functions, but it cries when it doesn’t know which one the programmer meant to call.
If you’re interested, the heirarchy is char -> int -> long -> long long
and float -> double -> long double
, but I don’t think you’ll need to know this.
This is the first type of ambiguity that can arise with overloaded functions. Note that the compile error only arises if I try to make an ambiguous function call: the declaration and definition of ambiguous overloaded functions is not a problem.
Another type of ambiguity is when there are multiple arguments and therefore multiple type conversions that may need to happen in order to make things line up.
1void print(int i, double d) {
2 cout << "Integer first: " << i << endl
3 << "Double second: " << d << endl;
4}
5
6void print(double d, int i) {
7 cout << "Double first: " << d << endl
8 << "Integre second: " << i << endl;
9}
10
11int main() {
12 print(3, 7.7); // no problem
13 print(4.3, 9); // no problem
14
15 char c = 'c';
16 print(c, 7.7); // convert c to an int, no problem
17
18 print(3, 7); // convert the 3 or the 7?
19 return 0;
20}
In the first two calls to print
, C++ can unambiguously identify which print
the user wanted to call. When we called print(c, 7.7);
, it can either convert c
to an integer and use the first verison, or it can convert c
to a double and 7.7
to an integer to use the second. Since the first one took fewer conversions, C++ will prefer the first version.
However, in the last call, C++ can either convert the 3 or the 7 to a double to match either version of print
. C++ doesn’t know which one to pick — they both look equally good — so it gives a compilation error and cries about it.
Remark 2.
The return type of a function does not influence how C++ chooses which version of an overloaded function to use. In fact, defining
gives a compilation error, as we’re providing C++ with a guaranteed ambiguity that can’t be resolved. This will give an error, even if an ambiguous function call is never placed!
Problem 3.
Determine the output of the following code. If there is a compilation error, describe it.
Problem 4.
Determine the output of the following code. If there is a compilation error, describe it. (This is a followup to the previous problem.)
Problem 5.
Consider the following program:
1#include <iostream>
2using namespace std;
3
4void f(int i, int j, double d = 0.75) {
5 cout << "First" << endl;
6}
7
8void f(int i, double d1 = 0.99, double d2 = 1.50) {
9 cout << "Second" << endl;
10}
11
12void f(double d1 = 0.5, double d2 = 0.9, double d3 = 1.7) {
13 cout << "Third" << endl;
14}
15
16int main() {
17 f(3, 3, 7); // (a)
18 f(0.9); // (b)
19 f(3.7, 1); // (c)
20 f(8, 3.3, 2); // (d)
21 return 0;
22}
Determine the outputs of lines (a) through (d) when run individually, or indicate that they produce compilation errors.