Reliable Software Logo

C++ In Action: Language

References

References to objects, friends, passing by reference, initializing a reference.


So far we've been exploring the has-a and is-a types of relationships between objects. The third kind of relationship is the has-access-to relationship. The has-access-to relationship is not "destructive" like the others. Objects to which A has access are not destroyed when A is destroyed.

Of course, every object has access to all the global objects and to all objects within the encompassing scopes. However, in order to access these objects, the object in question (more precisely its class) has to have built-in knowledge of the actual names of these objects. For instance, class World knows about the existence of the globally accessible object cout. There's nothing wrong with having a few global objects whose names are known to lower level objects. They form the scenery of our program. They can be used to abstract some globally available services. However, the need for one object to access another object is so prevalent and dynamic that the above mechanism is not sufficient. The has-access-to relationship finds its expression in C++ in the form of references.

A reference refers to an object. It is like a new temporary name for an existing object--an alias. It can be used just like the original object; that is, the syntax for accessing a reference is the same as that for accessing an object. One can pass references, return references, even store them inside other objects. Whenever the contents of the object changes, all the references to this object show this change. Conversely, any reference can be used to modify the object it refers to (unless it's a const reference).

We declare a reference variable by following the type name with an ampersand (actually, the ampersand binds with the name of the variable):

Istack & stack;

One important thing about a reference is that, once it's created, and initialized to refer to a particular object, it will always refer to the same object. Assignment to a reference does not make it point to another object. It overwrites the object it refers to with the new value. For instance, consider this code

int i = 5;
int j = 10;
int & iref = i;  // iref points to i
iref = j;

The execution of the last statement will result in changing the value of i from 5 to 10. It will not make iref point to j. Nothing can make iref point to anything other than i.

A reference cannot be created without being initialized. If a reference is stored within an object, it must be initialized in the preamble to the constructor of that object.

We'll use references in the definition and implementation of the stack sequencer. A sequencer is an object that is used to present some other object as a sequence of items; it returns the values stored in that object one by one. In order to retrieve values from the stack, the sequencer needs access to private data members of the stack. The stack may grant such access by making the sequencer its friend. All the stack's private data members are still invisible to the rest of the world, except for its new friend, the stack sequencer. Notice that friendship is a one way relationship: StackSeq can access private data of IStack, but not the other way around.

class IStack
{
    friend class StackSeq; // give it access to private members
public:
    IStack (): _top (0) {}
    void Push (int i);
    int  Pop ();
private:
    int _arr [maxStack];
    int _top;
};
class StackSeq
{
public:
    StackSeq (IStack const & stack);
    bool AtEnd () const;    // are we done yet?
    void Advance (); // move to next item
    int GetNum ()const;   // retrieve current item
private:
    IStack const & _stack;  // reference to stack
    int     _iCur;   // current index into stack
};

The constructor of StackSeq is called with a reference to a const IStack. One of the data members is also a reference to a const IStack. A const reference cannot be used to change the object it refers to. For instance, since Push is not a const method, the compiler will not allow StackSeq to call _stack.Push(). It will also disallow any writes into the array, or any change to _stack._top. A reference to const can be initialized by any reference, but the converse is not true--a reference that is not const cannot be initialized by a reference to const. Hence we can pass any stack to the constructor of StackSeq. The compiler, in accordance with the declaration of the constructor, converts it to a reference to a const IStack. Then _stack, which is also a reference to a const IStack, may be initialized in the preamble to the constructor of the StackSeq (remember, references must be initialized in the preamble).

Has-access-to

Figure 1 The graphical representation of the has-access-to relationship between objects.


Data member _iCur stores the state of the sequencer--an index into the array _arr inside the referenced stack. The sequencer being a friend of the class IStack has full knowledge of the stack's implementation.

StackSeq::StackSeq (IStack const & stack )
    : _iCur (0), _stack (stack) // init reference
{}

The implementation of the remaining methods is rather straightforward

bool StackSeq::AtEnd () const
{
    return _iCur == _stack._top;  // friend: can access _top
}

void StackSeq::Advance ()
{
    assert (!AtEnd()); // not at end
    ++_iCur;
}

int StackSeq::GetNum () const
{
    return _stack._arr [_iCur]; // friend: can access _arr
}

Notice that the dot syntax for accessing object's data members through a reference is the same as that of accessing the object directly. The variable _stack is a (read-only) alias for a stack object. Due to the sequencer's friend status, StackSeq is accessing IStack's private data members with impunity.

New logical operators have been introduced in this program as well. The exclamation point logically negates the expression that follows it. !AtEnd() is true when the sequencer is not done and false when it's done. You read it as "not at end."

The double equal sign '==' is used for equality testing. The result is false if two quantities are different and true if they are equal.

BugWarning: The similarity between the assignment operator = and the equality operator == is a common source of serious programming mistakes.

It is very unfortunate that, because of the heritage of C, a single typo like this may cause an almost undetectable programming error. Don't count on the compiler to flag the statement

x == 1;
or the conditional
if (x = 0)
as errors (some compilers will warn, but only at a high warning level). Some people try to prevent such errors by getting into the habit of reversing the order of equality testing against a constant. For instance
if (0 == x)
is safer than
if (x == 0)
because the omission of one of the equal signs will immediately cause a compiler error. I used to be one of the advocates of this style, but I changed my mind. I don't like a style that decreases the readability of code.

The not-equal-to relation is expressed using the operator !=, as in x != 0.

The main procedure tests the functionality of the sequencer. It pushes a few numbers onto the stack and then iterates through it. Notice how the object TheStack is passed to the constructor of the iterator. The compiler knows that what we really mean is a reference, because it has already seen the declaration of this constructor in the class definition of StackSeq. Since the constructor was declared to take a reference, the reference will automatically be passed.

When a formal argument of a method is specified as a reference, we say that this argument is passed by reference. When you call a method like that, you pass it a reference to a variable, and the method may change the value of that variable. This is in contrast to passing an argument by value. When you call a method that expects to get a variable by value, the method will not change the value of this variable. It will operate on a copy of its value.

Compare these two methods.

class Dual
{
    void ByValue (int j)
    {
        ++j;
        cout << j << endl;
    }

    void ByRef (int & j)
    {
        ++j;
        cout << j << endl;
    }
};

int main ()
{
    Dual dual;
    int i = 1;
    dual.ByValue (i);
    cout << "After calling ByValue, i = " << i << endl;
    dual.ByRef (i);
    cout << "After calling ByRef, i = " << i << endl;
}

The first method does not change the value of i, the second does.

If you are worried about the fact that from the caller's point of view it is not obvious whether a reference was meant--don't! As a general rule, assume that objects are passed by reference, built-in types by value (the example above notwithstanding).

Only in some special cases will we pass objects by value, and it will be pretty obvious why (see: value classes). So, when reading most programs, you can safely assume that, when an object is passed to a function, it is passed by reference.

void main ()
{
    IStack TheStack;
    TheStack.Push (1);
    TheStack.Push (2);
    TheStack.Push (3);

    for ( StackSeq seq (TheStack);
        !seq.AtEnd();
        seq.Advance() )
    {
        cout << "    " << seq.GetNum() << endl;
    }
}

Let us follow the path of the reference. A reference to TheStack is passed to the constructor of StackSeq under the (read-only) alias of stack. This is a new temporary name for the object TheStack. This reference cannot be used to modify TheStack in any way, since it is declared const. Next, we initialize the reference data member _stack of the object seq. Again, this reference cannot be used to modify TheStack. From the time the object seq is constructed until the time it is destroyed, _stack will be a read-only alias for TheStack. The sequencer will know TheStack under the read-only alias _stack.

Notice the creative use of the for loop. We are taking advantage of the fact that the for-loop header contains a statement, an expression, and a statement. The first statement--loop initialization--is executed only once before entering the loop. In our case it contains the definition of the sequencer. The following expression--the loop condition--is tested every time the loop is (re-) entered. If the expression is true (non-zero) the iteration starts (or continues). When it is false, the loop is exited. In our case the condition is that the sequencer is not at the end. The third statement--the loop increment--is executed after every iteration. In our example it advances the sequencer. Notice that after a normal exit from the loop, the loop condition always ends up false.

Any of the three parts of the for-loop header may be empty. An infinite loop, for instance, is usually implemented like this:

for (;;) // do forever
{
    // There must be some way out of here!
}
(Of course you can exit such loop using a break or a return statement.)

Here's a list of some of the operators that create Boolean values.

Equality Testing Operators
== equal to
!= not equal to
Relational Operators
> greater than
< less than
>= greater than or equal to
<= less than or equal to
Logical Negation Operator
! not

NextNext: Stack-based Calculator