Occasionally we get posts asking the differences between references
and pointers, and in the context of function calls, when to pass
parameters by reference, pointer, or value.
Difference between references and pointers
A pointer variable is a variable which holds the memory address of
some other variable. That "target" variable may be named or unnamed.
For example:
1 2 3
|
int i;
int* pInt = &i; // pInt "points to" i
int* pInt2 = new int; // pInt2 "points to" an unnamed int.
|
A reference variable is a variable which "refers to" another
named or unnamed variable. For example:
1 2 3 4 5 6 7 8
|
void foo( const std::string& str ) {}
std::string s1;
std::string& s1ref = s1; // s1ref "refers" to s1
// Here, we construct an unnamed, temporary string object to call foo.
// foo's "str" parameter now "refers to" this unnamed object.
foo( std::string( "Hello World" ) );
|
There are three critical attributes of pointers that differentiate them
from references.
1. You use pointer syntax to access the values "pointed to" by the pointer.
2. You can redirect the pointer to point it to a different "target" variable.
3. You can make a pointer point to nothing (ie, NULL pointer).
Examples:
1 2 3 4 5 6 7
|
int i, j;
int* pInt = &i; // pInt "points to" i
*pInt = 42; // This assigns the variable pointed to by pInt to 42
// So in other words, since pInt points to i, i now has
// the value 42.
pInt = &j; // This makes pInt now point to j instead of i.
pInt = NULL; // This makes pInt point to nothing.
|
Notice how use of an asterisk prior to the pointer variable accesses
the value being pointed to. This is called the
dereference
operator, a somewhat unfortunate name given that the language also
supports references and the dereference operator has nothing to do
with references.
Now to references. References have a couple of key characteristics
that differentiate them from pointers:
1. References must be initialized at the point of instantiation.
2. References must ALWAYS "refer to" a named or unnamed variable
(that is, you cannot have a reference variable that refers to
nothing, the equivalent of a NULL pointer).
3. Once a reference is set to refer to a particular variable, you
cannot, during its lifetime, "rebind" it to refer to a different
variable.
4. You use normal "value" syntax to access the value being referred to.
Lets look at some examples:
1 2 3 4 5
|
int i = 20, j = 10;
int& iref = i; // Instantiate iref and make it refer to i
iref = 42; // Changes the value of i to 42
iref = j; // Changes the value of i to 10 (the value of j)
iref = NULL; // Changes the value of i to 0.
|
So it seems like references are more limiting than pointers in that
two of the three characteristics of pointers are not available with
references. But in fact, these limitations tend to make programming
easier.
First of all, when writing generic template code, you can't easily write
a single template function that operates on values, references, and
pointers, because to access the value "pointed to" by a pointer requires
an asterisk whereas accessing a normal value does not require an asterisk.
Accessing the value of a reference works the same way as accessing a
normal value -- no asterisk needed. So writing templates that can handle
values and references is easy. Here's a real-world example:
1 2 3 4 5 6 7
|
template< typename T >
void my_swap( T& t1, T& t2 )
{
T tmp( t1 );
t1 = t2;
t2 = tmp;
}
|
The above function works great in these cases:
1 2 3 4 5
|
int i = 42, j = 10;
int& iref = i, jref = j;
my_swap( i, j ); // Sets i = 10 and j = 42
my_swap( iref, jref ); // Swaps i and j right back
|
But, if you are expecting with the following code that i will be set
to 10 and j to 42, then this doesn't do what you want:
1 2 3 4
|
int i = 42, j = 10;
int* pi = &i, *pj = &j;
my_swap( pi, pj ); // sets pi = &j and pj = &i
|
Why? Because you need to dereference the pointers to get to the
values being pointed to, and the template function above does not
have a single asterisk in it.
If you wanted this to work correctly, you'd have to write:
1 2 3 4 5 6 7
|
template< typename T >
void my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
}
|
And now in the above example, you'd use
my_ptr_swap( pi, pj );
to swap the values pointed to by pi and pj. Personally, I think this
solution stinks for three reasons. First, I have to remember two function
names instead of one: my_ptr_swap and my_swap. Second, my_ptr_swap is
harder to understand than my_swap because although they have the same number
of lines of code and effectively do the same thing, there are extra
deferences involved. (And I almost implemented the function wrong when I
wrote it). Thirdly, NULL pointers! What happens if one or both of the
pointers you pass to my_ptr_swap are NULL? Nothing good. In reality, if
I wanted to make my_ptr_swap robust, to avoid the crash, I'd have to write:
1 2 3 4 5 6 7 8 9 10
|
template< typename T >
void my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
if( t1 != NULL && t2 != NULL )
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
}
}
|
But this isn't exactly a great solution either, I suppose, because now
the caller of my_ptr_swap cannot be 100% sure the function did anything
unless they duplicate the if() check:
1 2 3 4
|
if( pi != NULL && pj != NULL )
my_ptr_swap( pi, pj );
else
std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;
|
But duplicating the check makes the check inside of my_ptr_swap kinda
pointless. But on the other hand, a function should always validate its
arguments. A conundrum. Perhaps a return value is in order:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
template< typename T >
bool my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
if( t1 != NULL && t2 != NULL )
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
return true;
}
return false; // false means function didn't swap anything
}
|
And now I can write:
1 2
|
if( my_ptr_swap( pi, pj ) == false )
std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;
|
Which is definitely better. But this simply introduces an extra error
leg in your program that you need to think about and deal with.
What
if my_ptr_swap fails? What should I do? In most trivial applications,
handling errors, if even done at all, are easy. But in larger applications
where you need to perform a sequence of 5 steps, each of which may fail
means you have to think about all of the following error legs:
1. What if operation #1 fails?
2. What if operation #2 fails? Do I roll back operation #1? What if
rolling back operation #1 fails?
3. What if operation #3 fails? Do I roll back #1 and #2? What if
rolling back operation #2 succeeds but #1 fails? What if rolling
back operation #2 fails?
4. What if operation #4 fails? .... etc ...
5. What if operation #5 fails? .... etc ...
There are a mind-boggling number of failure cases to think about
and test. Because it quickly gets complicated, most programmers
handle only one failure; double faults are often not handled very
gracefully (the program aborts in some way).
It seems to me that since error handling can easily dominate the design
effort and implementation effort, programmers should strive NOT to
artificially introduce error legs where they can easily be avoided.
One of the most prevalent error cases to deal with is that of a NULL
pointer. Enter references.
(to be continued)
Key characteristic number 2 of references, quoted from above:
2. References must ALWAYS "refer to" a named or unnamed variable
(that is, you cannot have a reference variable that refers to
nothing, the equivalent of a NULL pointer).
And this is the second reason why references should be preferred to pointers:
no NULL pointer checks! It eliminates one of the most common error cases in
all of C++ programming at once!
Now having said that, pointers cannot simply be avoided all the time. When
you dynamically allocate memory, pointers must be involved. But we can
mitigate this by some judicious programming.
Let's say I want to bubble sort a std::vector of ints. Ok, bad example given
that vector<> already has a sort method and so does the STL, but humor me.
One way to do this is:
1 2 3 4 5 6 7 8 9
|
std::vector<int> v;
// assume v is filled out with values
// This is REALLY suboptimal, but I'm writing this without testing it, and I
// want to ensure I get it right:
for( size_t i = 0; i < v.size() - 1; ++i )
for( size_t j = 0; j < v.size() - 1; ++ )
if( v[ i ] < v[ j ] )
my_ptr_swap( &v[ i ], &v[ j ] );
|
Students are taught that when a function needs to modify its parameters, you should
pass by pointer. That's great for languages like Pascal which don't support references,
but in C++, you have another option: pass by reference. In fact, if I replace the
my_ptr_swap call with
my_swap( v[ i ], v[ j ] );
it still works. AND,
I've eliminated the use of pointers.
Which brings me to....
When to pass parameters by value, by reference, and by pointer
In college, students are taught that there are two times you should pass by pointer:
1. If the function needs to modify its parameter;
2. If copying the variable to the stack to pass it to the function is expensive.
Well, in reality, neither of those is a great reason to pass by
pointer. In fact,
they are both great reasons to pass by
reference. In the first case, you'd
pass by non-const reference. In the second, you'd pass by const reference.
Constness is beyond the scope of this discussion. If you are not familiar with it,
a const reference is basically a read-only variable and a non-const reference is
a read-write variable.
But then, is there ever a reason to pass by pointer? Yes*. Sometimes, you truly
have an optional parameter. To use a horrible example from the C runtime library,
the time() function is declared as:
time_t time( time_t* tm );
If you pass a non-NULL pointer to time(), it writes the current time to the variable
pointed to by tm in addition to returning the current time. If you pass a NULL
pointer, then it does not do that. In other words, tm is essentially an optional
parameter. In some cases, passing a NULL pointer may cause the function to do
something different. For example, pthread_create() takes a pointer to a thread
attributes object. If you pass NULL, it uses the default attributes for the new thread.
If you pass a non-NULL pointer, it takes the attributes from the parameter.
Notice that in both cases I gave, NULL is an
expected, legal value that has use
to the programmer. A NULL pointer to my_ptr_swap is unexpected, and is considered
a
programming error on the part of the caller.
So, to summarize:
1. Pass by value when the function does not want to modify the parameter and the
value is easy to copy (ints, doubles, char, bool, etc... simple types. std::string,
std::vector, and all other STL containers are NOT simple types.)
2. Pass by const pointer when the value is expensive to copy AND the function does
not want to modify the value pointed to AND NULL is a valid, expected value that
the function handles.
3. Pass by non-const pointer when the value is expensive to copy AND the function
wants to modify the value pointed to AND NULL is a vlaid, expected value that the
function handles.
4. Pass by const reference when the value is expensive to copy AND the function does
not want to modify the value referred to AND NULL would not be a valid value if
a pointer was used instead.
5. Pass by non-cont reference when the value is expensive to copy AND the function wants
to modify the value referred to AND NULL would not be a valid value if a pointer was used
instead.
6. When writing template functions, there isn't a clear-cut answer because there are a few
tradeoffs to consider that are beyond the scope of this discussion, but suffice it to say that
most template functions take their parameters by value or (const) reference, however
because iterator syntax is similar to that of pointers (asterisk to "dereference"), any
template function that expects iterators as arguments will also by default accept pointers
as well (and not check for NULL since the NULL iterator concept has a different syntax).