Design Con 2015

An Introduction to References

February 26, 2001

Dan_Saks-February 26, 2001

An Introduction to References
In C++, references and pointers have overlapping functionality.Here are some insights to help you decide which to use for a particular task.

Both C and C++ provide pointers as a way of referring to objects indirectly. C++ also provides references as an alternative mechanism for doing what is essentially the same job. In a few situations requiring indirection, C++ insists that you use pointers. In a few others, C++ insists that you use references. But much more often than not, C++ lets you use either notation. The decision to use pointers rather than references, or vice versa, is often a matter of style.

It's been my experience that many C++ programmers don't have a clear sense for when they should use one notation rather than the other. This month, I offer some insights into why C++ provides references and why you might prefer to use them.

The basics

A reference declaration is nearly identical to a pointer declaration, except that a reference declaration uses the & operator instead of the * operator. For example, given:

int i = 3;

then:

int *pi = &i;

declares pi as an object of type "pointer to int" whose initial value is the address of object i, while:

int &ri = i;

declares ri as an object of type "reference to int" referring to i. Just as many programmers pronounce "int *" as "int star," they usually pronounce "int &" as "int ref." Initializing a reference to refer to an object is often described as "binding the reference to the object."

A key difference between pointers and references is that you must use an explicit operator-the * operator-to dereference a pointer, but you don't use an operator to dereference a reference. Once the previous declarations are in place, the indirection expression *pi derefences pi to refer to i. In contrast, the expression ri-without any operators at all-dereferences ri to refer to i. Thus, an assignment such as:

*p = 4;

changes the value of i to 4, as does the assignment:

ri = 4;

The C++ standard is very careful to avoid dictating how a compiler must implement references, but every C++ compiler I've seen implements references as pointers. That is, a declaration such as:

int &ri = i;

allocates the same amount of storage as a pointer, and places the address of i into that storage. The subsequent assignment using the reference:

ri = 4;

generates the same code as the assignment using the pointer:

*p = 4;

Neither performs significantly better.

References as parameters

In C++, you can declare function parameters with reference types. Consider the implementation of a function called swap, which accepts two int arguments and swaps the value of its first argument with the value of its second argument. For example:

int i, j;
...
swap(i, j);

leaves the value that was in i in j, and the value that was in j in i.

Here's one possible definition for the function:


void swap(int v1, int v2)
	{
	int temp = v1;
	v1 = v2;
	v2 = temp;
	}

This is simple and straightforward, but it doesn't work. The problem is that C++, like C, passes function arguments by value. That is, a call such as:

swap(i, j);

copies argument i to parameter v1 and argument j to parameter v2. The body of swap swaps the value in v1 with the value in v2, only to destroy v1 and v2 upon return. The values in i and j remain unaffected by the call.

In C, you must implement the function using pointer parameters, as in:


void swap(int *v1, int *v2)
	{
	int temp = *v1;
	*v1 = *v2;
	*v2 = temp;
	}

Then you write the call as:

swap(&i, &j);

This call passes the address of i rather than a copy of i. Ditto for j. In the body of swap, *v1 refers to i and *v2 refers to j, so the call does change the values of i and j appropriately.

In C++, you can use references in place of pointers as the function parameters, as in:


void swap(int &v1, int &v2)
	{
	int temp = v1;
	v1 = v2;
	v2 = temp;
	}

In this case, the call looks like:

swap(i, j);

At the time of the call, reference parameter v1 binds to argument i and reference parameter v2 binds to j. In the body of swap, v1 refers to i and v2 refers to j, so this call also changes the values of i and j appropriately.

Whether you define a working swap function with pointer parameters or reference parameters, the compiler should generate the same machine code. Again, the choice of notation is a matter of style.

Some programmers claim that the presence of the & in the function call adds clarity by making the argument passing mechanism more explicit. After all, the call:

swap(i, j);

could be passing its arguments by value or by reference. The call:

swap(&i, &j);

clearly passes its arguments by address.

Although this claim seems valid in this case, C++ does let you write functions for which you don't want to see those &s in the calls. This happens most often when working with overloaded operators, as shown in the following example, which involves enumeration types.

Operator overloading with enumerated types

In C++, as in C, enumerated types provide a simple mechanism for defining new scalar types. For example, suppose you have an application that deals with days of the week and months of the year. You can define a type day representing the days of the week as:


enum day
	{
	Sunday, Monday, Tuesday,
	Wednesday, Thursday, Friday,
	Saturday, not_a_day
	};

so that the constant Sunday has value 0, Monday has value 1, and so on.

Later in the program, you might like to write a loop such as:


day d;
d = Sunday;
while (d <= Saturday) 
	{
	// do something with d
	++d;
	}

This code compiles just fine in C, but not in C++. C++ compilers complain about the expression ++d appearing as the last statement in the loop body.

In C, each enumerated type is just an integer type. You can apply ++, or any other arithmetic operator, to a day just as you can to any integer. On the other hand, C++ regards each enumeration as a new type distinct from integers. The built-in arithmetic operators in C++ do not apply to enumerations. To maintain some backward compatibility with C, enumeration values in C++ implicitly convert to integer values. Thus, you can get the previous loop to compile simply by changing the type of object d from day to int:


int d;
d = Sunday;
while (d <= Saturday)
	{
	// do something with d
	++d;
	}

Now, the assignment d = Sunday converts Sunday to 0 and assigns it to d. The inequality d <= Saturday effectively compares d with 6.

Using int objects instead of enumeration objects weakens the compiler's ability to detect accidental conversions between different enumeration types. C++ offers a better approach. You can overload the ++ operator for type day. That is, you define a function named operator++ that accepts an argument of type day. Then, when the compiler sees the expression ++d, it translates the expression into the function call operator++(d).

Here's a first attempt at defining the function:


day operator++(day d)
	{
	d = (day)(d + 1);
	return d;
	}

For any x of arithmetic or pointer type:

++x;

is equivalent to:

x = x + 1;

For day d, the expression d + 1 converts d to an int before adding 1 (also of type int). The result is an int. Although C++ converts day to int, it won't convert int to day without a cast. Therefore, the assignment:

d = (day)(d + 1);

uses a cast to convert the result of the addition from int back to day before assigning the result to d.

As with the initial version of swap, this first version of operator++ doesn't do the job. The call operator++(d) passes d by value, so the call modifies a copy of d, not d itself.

You could define operator++ so that it passes its argument by address, as in:


day *operator++(day *d)
	{
	*d = (day)(*d + 1);
	return d;
	}

but then you must invoke the operator using expressions such as ++&d, which doesn't look quite right. The whole point of operator overloading is to make user-defined types look like built-in types when they behave like built-in types. The expression ++&d is not something you can write for any d of a built-in type. This is a case where writing the & in the function call actually reduces clarity.

The proper way to define operator++ is with references as both the parameter and return types, as in:


day &operator++(day &d)
	{
	d = (day)(d + 1);
	return d;
	}

Using this function, expressions such as ++d have the proper appearance as well as the proper behavior.

The key insights

I believe the key insight into why C++ has reference types as well as pointer types is that reference types enable overloaded operators to look like built-in operators, as well as act like them.

Pointers can do almost everything that references can do, but they can lead to expressions that don't look right. On the other hand, references have some restrictions that make them less convenient than pointers for implementing algorithms and data structures. References reduce, but do not eliminate, the need for pointers.

As a general rule, use references in function parameters and return types to define attractive interfaces. Use pointers to implement algorithms and data structures.

I'll have more to say about references and pointers in upcoming columns.

Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. He is also a consulting editor for the C/C++ Users Journal. He served for many years as secretary of the C++ standards committee. With Thomas Plum, he wrote C++ Programming Guidelines. You can write to him at dsaks@wittenberg.edu.

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER