5.3. Comparison with references

Recall from our earlier discussions of pass by reference that the address of operator & allows us to pass by reference:

 1#include <iostream>
 2
 3void by_value(int x) {
 4  x = 99;
 5  std::cout << "in by_value the address of x is "
 6            << &x << '\n';
 7}
 8
 9void by_reference (int& x) {
10  std::cout << "in by_ref the address of x is   "
11            << &x << '\n';
12  x = -1;
13}

In function by_value the statement x = 99; changes the copy provided. The value of x is printed, but is destroyed when x goes out of scope on line 6.

No special character is needed if you want to use a function that takes a reference:

 1using std::cout;
 2
 3int main () {
 4  int beta = 11;
 5  cout << "the address of beta is "
 6       << &beta << '\n';
 7
 8  by_value(beta);
 9
10  cout << "beta = " << beta << '\n';
11
12  by_reference(beta);
13
14  cout << "beta is now "
15       << beta << '\n';
16}

References do have some definite advantages:

  • A reference must always be initialized using an existing object. In other words, a reference can never be null.

  • A reference can't be reassigned to a different object

  • A const reference means you can't modify the thing the reference refers to

  • References are simpler, more limited, and inherently safer than pointers

However, there are important things you can't do with references:

  • You can't assign an address to a reference

    • This would have the effect of having a reference refer to a different object

    • The technical term for this is that references are not assignable

  • You can't operate on a reference

    • In other words, you can't increment the referred to memory address, which, by definition, would involve having the reference refer to a different object

  • You can't use a single reference to refer to more than one object

  • You can't use references in containers such as vector

    • Containers can only hold assignable entities

We still need to be able to do all these kinds of memory manipulations. In C++, we achieve these goals using pointers.

C++17 Feature

C++17 introduced the std::string_view template. A string_view is a non-owning read-only view of character data. It is implemented as a pointer to a character sequence and a size. It is useful for read-only function parameters, but it depends on the original text remaining alive. For example:

 1#include <iostream>
 2#include <string>
 3#include <string_view>
 4
 5// note we normally pass string_view by value
 6void print_label(std::string_view label) {
 7  std::cout << label << " has "
 8            << label.size() << " characters\n";
 9}
10
11int main() {
12  std::string course = "CISC 187";
13
14  print_label(course);
15  print_label("Data structures");
16  print_label(std::string_view{course}.substr(5));
17}

5.3.1. Function passing semantics

We can pass pointers to a function that expects a reference:

 1#include <cassert>
 2
 3void by_reference (int& x) {
 4  x = -1;
 5}
 6
 7int main() {
 8  int  i = 5;
 9  int* p = &i;
10  by_reference(p);
11  assert (i == -1);
12  return 0;
13}

If we pass in only p, what happens?

Show Answer

The program fails to compile.

We can't pass an int* to a function expecting an int&.

Non-const references vs. pointers

Some programmers consider passing by non-const reference bad style, because the call syntax is the same as pass by value. When a variable is passed into a function by non-const reference there is no visual indication to the programmer of what to expect. Without reading additional documentation or reading the source code, there is no way to know if the function will change its parameter or not.

void func (int& x);

int main() {
  int x = 5;
  func(x);       // will x change?
}

For this reason, a function that takes a non-owning pointer is preferred:

void func (int* x);

int main() {
  int x = 5;
  func(&x);       // Caller expects x to change
}

A function signature is a contract between the function author and the function caller. A function that takes non-const references represents a poorly written contract. Callers don't know what to expect when the function is called. Even if the parameter isn't changed today, it might tomorrow. A non-owning pointer makes the intent clear. There is still no requirement to change the parameter, but since the caller is explicitly passing in an address, they can expect it to change.


More to Explore