References, overloading and default arguments

Introduction

C++ gives functions three new tools. References let a function change its caller's variables without the pointer syntax of C. Overloading lets one name cover several related jobs, and default arguments let a call leave out the values that are usually the same.

How it works

References

#include <iostream>
using namespace std;

void swapByValue(int a, int b) {
    int t = a;
    a = b;
    b = t;
}

void swapByRef(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}

int main() {
    int x = 1, y = 2;
    swapByValue(x, y);
    cout << x << " " << y << endl;
    swapByRef(x, y);
    cout << x << " " << y << endl;
    int& alias = x;
    alias = 10;
    cout << "x is now " << x << endl;
    return 0;
}
Open in compiler

int& a declares a reference: another name for an existing int. When swapByRef(x, y) runs, a is x and b is y, so swapping them swaps x and y. In the visualizer a reference is drawn as an arrow to the variable it names. swapByValue gets copies instead, so x and y do not change.

A reference does the job of a C pointer parameter (int *a and *a = ...) with less noise: no & at the call and no * inside. Two rules make it safe:

  • a reference must be bound to something when it is created (int& r; is an error);
  • it can never be re-bound: alias = 10 stores 10 in x, it does not make alias refer to 10.

const references (const string& s) let a function read a big object without copying it, and promise not to change it. They can also take temporary values, such as "text" or a + b.

Syntax

void swapRef(int& a, int& b);              // a and b are the caller's variables
double total(const vector<double>& v);     // read without copying
int area(int side);                        // overloads: same name,
int area(int w, int h);                    // different parameters
void greet(string name, string greeting = "Hello");

Example

#include <iostream>
#include <string>
using namespace std;

double area(double r) { return 3.14159 * r * r; }   // a circle
int area(int w, int h) { return w * h; }            // a rectangle
int area(int side) { return side * side; }          // a square

void greet(string name, string greeting = "Hello") {
    cout << greeting << ", " << name << "!" << endl;
}

int main() {
    cout << area(2.0) << " " << area(3, 4) << " " << area(5) << endl;
    greet("Rahim");
    greet("Karim", "Assalamu alaikum");
    return 0;
}
Open in compiler

C++ allows several functions with the same name if their parameter lists differ: this is overloading. The compiler picks the version whose parameters best match the arguments: area(2.0) takes a double, so the circle version runs; area(3, 4) has two ints.

A parameter with a default argument (string greeting = "Hello") can be left out at the call. Defaults must come at the end of the parameter list.

Line by line

  1. Three functions are all called area. One takes a double, one takes two ints, and one takes one int.
  2. greet has a default argument, so its second parameter can be left out.
  3. area(2.0) has a double argument, so the circle version runs and gives about 12.5664.
  4. area(3, 4) has two ints, so the rectangle version runs and gives 12. area(5) has one int, so the square version gives 25.
  5. greet("Rahim") leaves out the greeting, so "Hello" is used.
  6. greet("Karim", "Assalamu alaikum") gives both arguments, so the default is not used.

Expected result

12.5664 12 25
Hello, Rahim!
Assalamu alaikum, Karim!

In the real world

The standard library is full of overloads: to_string works for every number type, and abs and max have versions for each type. Large objects such as strings, vectors and images are passed by const& throughout real C++ code, because copying them would waste time and memory.

Common mistakes

  • Passing a value to a non-const reference: swapByRef(x, 5) is an error, because 5 is not a variable.
  • Overloads that differ only in their return type are not allowed: the call would not tell them apart.
  • Returning a reference to a local variable: it refers to memory that is gone when the function ends.

Best practices

  • Pass small built-in values such as int by value, and large objects by const&.
  • Use a non-const reference only when the function is meant to change the argument.
  • Keep overloads doing the same kind of job, so a reader is never surprised.

Summary

A reference is another name for a variable, so a function can change its caller's values, or read them without copying when the reference is const. Overloading reuses a name for different parameter lists, and default arguments fill in what a call leaves out.

Quick check

1. int x = 1; int& r = x; r = 5; What is x?
2. Which call uses void f(int a, int b = 2)?
3. Why pass a big object as const T&?

Key takeaways

  • A reference must be bound when it is made, and cannot be re-bound.
  • const T& reads a big object without copying it.
  • The compiler picks an overload from the arguments' types.
  • Default arguments must come at the end of the parameter list.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.