Section 1 - A Constant Cause of Compiler Errors

Textbook Reading

text

After a first reading of this week's modules, Read the textbook, Chapter 1, lightly. Only those topics covered in the modules need to be read carefully in the text. Once you have read the corresponding text sections, come back and read this week's modules a second time for better comprehension.

2A.1.1 const

One of the most common reasons for compiler errors -- and student questions -- is the const modifier. This one term has disparate meanings depending on where it appears in programs. As a result, in my classes I use it less often than I would in a real project, because we normally have enough to worry about without chasing down const incompatibilities. Nevertheless, it is real C++ and you will have a need for it someday, so I suppose a section that clears the air is in order.

This section may seem dry and tiresome, especially toward the end. The final sections are really for only the most advanced among you. Stay calm. It is important for you to read it, but it is not necessary for you to memorize it. Think of this page as a reference that you read (and print) once and come back to as needed.

2A.1.2 Local Variables and const

We start simply and move slowly toward the complex, as always.legos

Using the const modifier on a local variable is a promise to never change it after the initialization (which must accompany the declaration).

Any attempt to declare a const modifier without initialization, or to modify it later in the program, will result in a compiler error. This is what we want - we define constants specifically for this purpose - it enables the compiler to find logic errors wherein the programmer inadvertently reassigns a variable they want to be unchanged.

const int unChangeable = 12;
const int unChangeable2;   // error!
const iTunesEntry fixedTune;  // okay because default constructor implied

cin >> unChangeable; // error!
unChangeable = 12;   // error!
fixedTune.setArtist("Miles Davis");  // error!

You might think that we have an error in the above - we seem to be declaring a const object, fixedTune, without the required initialization. In fact, with user-defined objects we have an implied initialization: the default constructor.

I like to use ALL_CAPS for most primitive constants so that their meaning is easy to guess in the program:

const int UN_CHANGEABLE = 12;

2A.1.3 Pointers and const

There are two ways to use const with pointers. You have to ask yourself "do I want the const to prevent the pointer from changing or the thing pointed to from changing?" Most commonly it is the latter.

const iTunesEntry *fixedTune;   // that which is pointed-to is const

fixedTune = new iTunesEntry();  // ok
fixedTune->setTitle("I Thought About You");  // error!
fixedTune = new iTunesEntry();  // re-assign as many times as you want.

If you want to prevent the pointer from being changed, place the const modifier to the right of the *, as in:

iTunesEntry * const fixedTune = new iTunesEntry;  // pointer is const

// fixedTune = new iTunesEntry();  // error
fixedTune->setTitle("I Thought About You");  // no prob, object not const

cout << tunesInput.getFileName() << endl;
cout << tunesInput.getNumTunes() << endl;

As you see, you can const either the pointer or the pointed-to independently of one another. Of course if change in any form is to be outlawed, do both:

const iTunesEntry * const fixedTune = new iTunesEntry; // maximum unchangeability 

How do you remember whether it is the pointer or that which is pointed to which we are trying to make a constant? Read from right-to-left. The const refers to the entire construct on the right. For example, in:

iTunesEntry * const fixedTune = new iTunesEntry;

the const refers to the identifier fixedTune, which is the pointer. It is the pointer that cannot be changed. However,

iTunesEntry const *fixedTune;

the const refers to the entire construct *fixedTune which means "that to which fixedTune points". By the way, most organizations prefer the equivalent:

const iTunesEntry *fixedTune;

which is what I used above to "constantize" the object being pointed at. Basically, you can ignore the iTunesEntry type name in the analysis since it is not a variable. It is the variable and the * that you have to place in the correct relation with the const modifier.

2A.1.4 References and const

legos 2

Normally, we don't use references unless we are declaring formal parameters, but before I get into functions, we can talk about the use of const on local reference variables. First, let's remember what a reference variable is.

   double x;
   iTunesEntry tune;

   double &dubRef = x;
   iTunesEntry &tuneRef = tune;

After that, both x and dubRef refer to the same double, and both tune and tuneRef refer to the same iTunesEntry. However, there is only one double and one iTunesEntry, so if you change x, you will find dubRef changed and vice versa. Similarly, you can call tuneRef.setArtist() to change the artist and you would find that tune's artist was changed. This is like a pointer, but we use ordinary object notation and no * or -> with reference variables. They act like pointers but look like ordinary variables.

So much for the tutorial on reference variables. What does const do in the following?

   iTunesEntry tune;
   const iTunesEntry &tuneRef = tune;

It means that you cannot use the reference tuneRef to change the object. That doesn't mean you can't change the object. You can use some other reference to do so, and in particular, you can use the original variable name, tune to change it. To wit:

   tune.setArtist("Jewel");      // fine
   tuneRef.setArtist("Jewel");  // error

Just like we have two ways to const-up the pointer's object, there are two ways to const-up the referenced object, and they are equivalent. We see one, above. The other is:

   iTunesEntry const &tuneRef = tune;

Reading from right-to-left and only looking at variables, not type names, we see that this is the same as our previous syntax.

2A.1.5 Global Scope Functions and const

Programmers pass parameters by reference for one of two reasons:

  1. They want the function to modify the parameter value persistently (i.e., on the client side after the call is complete)
  2. They want to avoid the overhead -- including copy constructors and destructors -- of passing a big object down into a method.

Sometimes they want to accomplish both, which is fine, but when they have no interest in changing the parameter value, and avoiding overhead is the only concern, then the const modifier will be helpful in catching unintended modifications to the parameter within the function. When you use the const modifier, any unintended changes are caught by the compiler, making debugging easier. In fact, this is the most common use of const in C++.

Without the const modifier, if I pass a reference parameter, I could do damage to the data by accident. Below, I called setTitle() rather than getTitle() to emulate a mistake by the programmer:

void DisplayOneTune(iTunesEntry &tune)
{
   cout << tune.getArtist() << " | ";
   cout << tune.setTitle("What Was I Thinking?") << " | "; 
   cout << " " << tune.convertTimeToString() << endl;
}

The compiler doesn't catch it, the output is odd and wrong, and worst of all, I have changed the data in the tune, permanently. If we change the signature to be a const reference, then the compiler will find the error, and we'll have to fix it before we run:

void DisplayOneTune(const iTunesEntry &tune)
{
   cout << tune.getArtist() << " | ";
   cout << tune.setTitle("What Was I Thinking?") << " | ";  // compiler error, thankfully
   cout << " " << tune.convertTimeToString() << endl;
}

When you start using const reference parameters you'll find that you may have to modify some previously written code to make it compile. That's because once the compiler detects a const reference, any attempt to call a method (even an accessor) via that reference had better be declared a const method, or the compiler senses an illegal attempt to modify the object -- even if no such attempt was made. So, if you want to use const references, it's a good idea to start using them from the ground floor of the project. That way you will not need to do any back-tracking. More about this in the next section.

By the way, you'll need the const in both the prototype and the definition signature. Const-up both or neither.

2A.1.6 Return Values and const

Returning const values or references from global scope functions doesn't give particularly strong protection and have a limited use. We can, for example, assign the return value of the function to a non-const variable, and use that to change any data we wish. The const will merely prevent the function invocation, itself, from being used to modify the object. Let's imagine a global-scope function that gets an iTuneEntry from the user and returns it as a const. Without seeing the details, here's its prototype:

   const iTunesEntry getTuneFromUser();

As you can see, below, this does not prevent us from changing the return value, but it does prevent an immediate change at the invocation site:

   // capture the result and do what you want.  const return has no power
   userTune = getTuneFromUser();
   userTune.setArtist("Lil' Wayne");
      
   // but you can't do this
   getTuneFromUser().setArtist("Lil' Wayne");

legos 3The inclusion of an & to make the return parameter a reference doesn't change this. In fact, returning a reference in a global-scope function has limited use because you have to have a persistent object to pass back. That means either

All this is a way of saying that, const return types, whether value or reference, are not used much on global-scope functions. I present them because they are useful in certain situations in class instance functions, though, one of the most important being a member function operator[] (int k), i.e., overloading the brackets operator. Because of this, I'll defer any more discussion about const return types for now and come back to them when we cover the [] operator.

2A.1.7 Class Accessors and const

When we are prototyping or defining a class accessor or other non-mutator method, we don't intend to modify any object members. The way to tell the compiler to enforce this wish is by declaring the function, itself, to be const. You do this at the end -- not the beginning -- of the signature. Here it can be seen in the class prototype:

   // accessors
   string getTitle() const { return title; }
   string getArtist() const { return artist; }
   int getTime() const { return tuneTime; }

   // helpers
   string convertTimeToString() const;

And since the last method was only prototyped, we'll need to add it again in the definition:

   string iTunesEntry::convertTimeToString() const
   {
      ...
   }

I want to return to something I said earlier about doing consts up-and-down-the-line. In a previous section, if I had made DisplayOneTune() take a const reference, but failed to make our accessors const, we would get an error. So we need both. Accessors should be first declared to be const, and then in the global scope function we can (but are not required to) declare the reference parameter const. Since declaring your accessors const does not require that you use const reference parameters in your client's global scope functions, making the accessors const is a safe and flexible approach.

By the way, everything in this section only pertains to member functions. A global-scope function can't be const since there is no meaning to the phrase "we don't intend to modify any object members" for non-member functions.

As a side effect, a const accessor is considered part of the function signature, distinct from the return type. While we may not overload a function solely based on a different return type, we can use the const as described this section to differentiate two member methods. Thus, we could have this in the same program:

   int getTime() const { return tuneTime; }
   int getTime()  { return 55; }

While this example is a little silly, the point is that there are two different methods here, and one, the second, could modify the object, while the first could not. Which one gets invoked in a particular situation "in the field?", a relevant question since they have the same parameter list (namely, void)? If the method were called by dereferencing a const reference, then the const version would be called. If the method were called through a normal reference or variable, the non-const version would be called.

This is especially useful when defining two versions of the brackets operator, [], one that is intended to be used on the Right Hand Side (RHS) of an assignment -- the const version for reading -- and one intended to be called from the Left Hand Side (LHS) of an assignment -- a non-const version for writing.

2A.1.8 Instance Methods and const Reference Return Types

We come to the acme of the lesson: how one returns a class member datum in one of two ways:

The answer is easy, although you have to read all of the above (and below) explanations to really understand it. So I'll give the answer first, and you can have the explanations for dessert. If you want the client to be able to modify the return value, don't put consts anywhere. If you do not want them to be able to change it, make both the return type and the function a const. Here is an example of both situations using the operator[] function, i.e., the bracket operator:

   Object & FHvector::operator[]( int index );
   const Object & FHvector::operator[] (int index ) const;

Now for some explanation.

  1. The "container" class FHvector is not visible here so you are not completely going to be at ease about what these two functions are returning. I'll help: the class contains a private member array, and we are returning the kth element of that array to the user. This is very common, whether we use arrays, vectors or anything else. Brackets are usually used to "pick-out" one of several internal elements contained within a container class (actually within an object in the container class).
  2. We are returning a reference in both cases. This allows the client to access, for read or write, the data stored deep within the container class FHvector.
  3. In the first case, if the client wants to, it can modify the element. This seems a bit dangerous since we are allowing the client to directly change our internal elements. In practice, however, those elements are objects of some smaller class (like Employee or iTunesEntry) and the client would still have to go through that smaller class's accessors, so the protection is still there.
  4. In the second case, the client cannot change the returned value.
  5. Because the const at the end of the second line is part of the signature of the function, and we have already seen that this can serve to overload the function, both of these variants can, and will, be used in a single class, simultaneously.
  6. The reason we want both versions in our container class is to give clients the freedom to have const objects or normal objects used with the [] operator, as in:
  7. FHvector a;
    const FHvector b;
    	
    for (k = 0; k < 100; k++)
       a[k] = b[k]