Tuesday, November 7 and Thursday, November 9, 2006
const Correctness

Topic

Constants in C++

- use of #define in C++ is highly discouraged because it can result in incorrect expressions
- Constants can be created from any variable by adding the keyword const  
- Constants are sometimes called "constant variables" because that is what they are, just variables that never change.
- Constants always must be initialized, using standard C variable initializations.

- Constants can be global, declared in the .h file.  This is considered good programming since a constant never changes and therefore rarely causes bugs in your program.
- You can also have constant instance variables inside a class.  They can be either public, protected, or private, as your design judgment dictates.  All are considered good programming.
- Constant instance variables are initialized in the constructor with the same syntax that you can use to initialize other instance variables.
-  For good programming style you should write constants as all CAPs, as you do in C. 
- Example Syntax:

const double PI = 3.14159;
const int MAXLENGTH = 256;

class Square
{
public:
    Square(int);

    const int NUMBEROFSIDES;
private:
    const double SQUAREROOTOFTWO;
    int sidelength;
};

Square::Square(int s): sidelength(s), NUMBEROFSIDES(4),
  SQUAREROOTOFTWO(1.414)
{
}

Constant pointers and the values they point at.

- Use of the keyword const with pointers can be a little confusing, because is the pointer itself constant, or what it points to constant.  
- To keep these two concepts separate in your mind, always read the data type backwards!  
- For instance, with the following declaration:

const double PI = 3.14159
const double* PI_PTR = Π
 

you should read the second line as "PI_PTR is a pointer to a double constant".  That clearly indicates that it is the value PI_PTR is pointing at that is constant, not the pointer itself.

- If you have the following declaration:

const double PI = 3.14159
double* const PI_PTR = Π
 

read it as "PI_PTR is a constant pointer to a double".  That clearly indicates that it is the address inside PI_PTR that is constant and cannot be changed.  What PI_PTR points at, however, could be changed.  (A different value for PI??  Sounds like a bug to me.)

- You could also have:

const double PI = 3.14159
const double* const PI_PTR = Π
 

which means "PI_PTR is a constant pointer to a double constant".   With that declaration, nothing can change!


Constant parameters

- A formal parameter in a function definition is just a local variable declaration, and therefore it can be declared constant.
- A constant formal parameter cannot be on the left side of an assignment statement inside the function.  In other words, you cannot try to change the constant parameter.  To do so is a compiler error
- Also, a constant formal parameter cannot be passed in a function call within your function definition, unless it is also passed to another constant formal parameter.  To try to use a const parameter in a nested function call to a non-const parameter will cause a compiler error. 
 

- Either a variable or a constant can be passed to a constant formal parameter, no problem.  Constant parameters are very safe and should be used whenever possible.  

void printline(const char* string)
{
    cout << string << endl;
}

- Notice in the previous function,  string is passed by a pointer.  Without the  const  would be pass-by-reference, but it is now essentially passed by value because it will be a compiler error if the code tried to change the string.  
- The converse, however, will have a problem.  Passing a constant to a formal parameter variable, however, does not make sense.  Constants can not convert into variables.  How could you change PI? 
- If you have the following declaration:

const double PI = 3.14159

and the following function:

void increment(double x)
{
   x += 1.0;
}
  

the function would try to increment PI. 

 

const correctness

- Historically the keyword  const was only occasionally used.  It was part of early C, but most constants were declared using  #define. It was also occasionally used to make sure a pointer did not modify what it pointed at. 
- In the early days of C++,
 const
was used much the same way, but during the 1990's, "const correctness" became more popular.  The C++ compiler was designed to very carefully catch all errors of passing a constant to a variable parameter.  This forced programmers to choose one of two styles -- either ignore using   const in most parameters, or very carefully using const everywhere it is needed. 
- As a practical matter, once you start using
const in parameters, you must carefully analyze everywhere it is needed in your code, or you will get compiler errors.
- The problem is particularly bothersome when you take code that was written without using
const thoroughly, and then try to add const where you think it is appropriate, the compiler gives lots of errors, because it demands that const be used correctly everywhere.
- If you add
 const to the formal parameter in a function that calls a number of other functions, then all the function your function calls must also have const in their parameters if you want to pass the constant to them.
- For instance, say you have the following function definitions:

#define NONE -2000000000
#define EVEN 0
#define ODD  1
#define SIZE 1000
 

// copy odd values from original to odd, and even values
//   from original to even.  If there are no odd or even
//   values, put NONE as first value in the appropriate array. 
void copy_odds_and_evens(int original[], int size, int odd[], int* oddsize, 
                                                   int even[], int* evensize)
{
    // check for no odds, then copy or set errors
    if (!nonefound(original, size, ODD))
        copy(original, size, odd, oddsize, ODD);
    else
        odd[0] = NONE;
    // check for no evens, then copy
    if (!nonefound(original, size, EVEN))
        copy(original, size, even, evensize, EVEN);
    else
        even[0] = NONE;
} 
bool nonefound(int array[], int size, bool checkodd)
{
    bool none = true;
    int i;
    // modulo 2 gives 1 for odd, which is interpreted as true
    // if checking for evens, add one to reverse the logic
    for(i = 0; i < size; i++)
        if ((array[i] + (checkodd ? 0 : 1)) % 2)
            none = false;
    return none;
} 
void copy(int from[], int fromsize, int to[], int* tosize, bool checkodd)
{
    int fromindex;
    *tosize = 0;
    for (fromindex = 0; fromindex < fromsize; fromindex++)
        if ((from[fromindex] + (checkodd ? 0 : 1)) % 2)
            to[(*tosize)++] = from[fromindex];
} 

- This is typical code that is not const correct.  Notice that const is not used anywhere.  However, notice that   int original[] is never changed, nor is it intended to be.  The original array is intended as a read only source.  Likewise, int size and bool checkodd are also not meant to be changed, but .

- Const correctness analysis would go like this:
      
int size and bool checkodd are passed by value, so it does not matter if they are changed inside the function.  Adding the keyword const is not needed for pass-by-value parameters.
      
int even[], int odd[], int to[] and
int* tosize are meant to be changed and are passed-by-reference.  They are correct as written.
      
int original[] is passed as a pointer, even though it is not intended to be changed.  It should be declared const, as in the following:
void copy_odds_and_evens(const int original[], int size, int odd[], int even[], int* newsize)

However, if that is all you change, you get the following compiler errors:

passing `const int *' as argument 1 of `nonefound(int *, int, bool)' discards qualifiers
passing `const int *' as argument 1 of `copy(int *, int, int *, int *, bool)' discards qualifiers
passing `const int *' as argument 1 of `nonefound(int *, int, bool)' discards qualifiers
passing `const int *' as argument 1 of `copy(int *, int, int *, int *, bool)' discards qualifiers

Once you declare a
 const parameter, you can only pass it to another  const parameter.  This is only a nuisance, however, because the  nonefound
and  copy functions also do not change their array and from parameters, which original is passed to.  Therefore you can eliminate the compiler errors if you add const to these functions also. 

bool nonefound(const int array[], int size, bool checkodd)
void copy(const int from[], int fromsize, int to[], int* tosize, bool checkodd)


Now your constant array
 const int original[] is passed only to the constant arrays const int array[] and const int from[].  There will be no compiler errors.

- Even if you have a large program and get a lot of compiler errors, you should write a const correct program.  The compiler is helping prevent inadvertent changes to variables not meant to be changed. 
- Once you get used to const correctness, you will automatically analyze your functions while writing them.  Adding
const to any pointer parameter that is not changed within your function, or can not be changed by any function your function calls, will become a normal part of writing your program.  The result will definitely be better code.

If you add a  const to protect the array parameter, as you should because it is not changed inside the function,

int* reportifanynegatives(const int array[], int size)

you will get the following compiler error. 

return to `int *' from `const int *' discards qualifiers

Since the array is passed into the function as a const, it must be returned as a const.  

const int* reportifanynegatives(const int array[], int size)
 


const references

- Since passing a large object by value involves copying a lot of data, with calls to a copy constructor.  This is very inefficient, and really unnecessary.  You can pass by reference using a pointer to an object constant, similar to the arrays above.  However, since references have been added to C++, and they are much more convenient than pointers, passing by a reference to a constant object has become much preferred over passing an object by value.  Here are the code comparisons:

// this code is pass by value, not recommended
void analyzebigobject(BigObject theobject)
{
    // code does not change theobject
}

// this code will pass by a pointer to a const object
//   it will work, but is less convenient then the
//   next example
void analyzebigobject(const BigObject* theobject)
{
    // theobject must be dereferenced to be used
    //    theobject->dosomething()
    // dosomething() must be a const function, however. (see below)
}

// this is the preferred way over pass-by-value
void analyzebigobject(const BigObject& theobject)
{
    // much more convenient
    //    theobject.dosomething()
    // dosomething() must still be a const function. (see below)
}

 

const functions

- A const function is a function that does not change any instance variables in the object.  For instance, standard setter functions are not const functions because they change an instance variable.  A getter function, however, merely returns a value within the object without effecting the variable that contains that value.  The getter function should be declared as  const.  Here is the syntax:

int AClass::getValue() const
{
    return value;
}

- Notice where the keyword 
const is placed, after the function name and parameter list.  Placement of   const there means that this is a constant function - that is, it does not change the instance variables inside the object. 
- const functions are important, because if an object is declared 
const then only constant functions can be called on that object.  In the example above, since const BigObject& theobject declares theobject as a constant object, then the call to  theobject.dosomething() will cause a compiler error unless dosomething() is declared as a constant function.  This protects a constant object from being changed by a call to one of its internal functions.  The declaration of dosomething must have this syntax:

void BigObject::dosomething() const
{
    // this code can not change an instance variable inside this class
}

- notice that even though there are no parameters or a return value to this function, it could still change the object.  If it doesn't, then you should declare it  const.
- The compiler checks if any instance variables are changed by the function, and gives an error if you try to declare it const when it isn't.
- If a constant function inside a class, calls any other functions within that class, they also most be
 const
- This one of the most neglected parts of const correctness, but it leads to obnoxious compiler errors unless you are consistent about declaring all constant functions as
 const if that is what they are.



const return types

- Occasionally you will want to return a  const parameter as the return type.  For instance, suppose you want to create a function that prints reports about an array, and  could be chained together with similar functions, you would write this:

int* reportifanynegatives(int array[], int size)
{
    int i;
    int negativescount = 0;
 
    for (i = 0; i < size; i++)
        if (array[i] < 0)
            negativescount++;

    if (negativescount > 0)
        cout << negativescount << " negatives found." << endl;

    return array;
}

If you add a  const to protect the array parameter, as you should because it is not changed inside the function,

int* reportifanynegatives(const int array[], int size)

you will get the following compiler error. 

return to `int *' from `const int *' discards qualifiers

Since the array is passed into the function as a const, it must be returned as a const.  

const int* reportifanynegatives(const int array[], int size)
 


The ultimate const - a postscript.

- O.K, here is the ultimate const code.  It does compile:

class SomeClass
{
public:
    const int* const SomeClass::somefunction(const int* const parameter) const;
    // 5         4                             3           2               1
};

- This function prototype is best read backwards:
"This is a constant function, with a parameter that is a constant pointer to an integer constant, that returns a constant pointer to an integer constant."    Whoo.

-  This is what each of the 
const's mean, from back to front:
      
const 1: This function does not change any instance variables inside the object.
      
const 2: This parameter's pointer address can not be changed inside this function.
      
const 3: This parameter points to an integer that can not be changed inside this function.
      
const 4: This function returns a pointer whose address can not be changed in the calling function.
      
const 5: This function returns a pointer to an integer that can not be changed in the calling function.

-  Understand all of that and you can truly be a const correct programmer.



ACK!  -  another postscript.

- C and C++ are not strongly typed, and therefore there are lots of tricks programmers can do to circumvent normal protections.  For instance, a  const integer can actually be changed!  Here is the code:

const my_age = 32;

*(int*)&my_age = 35;


Readings

Deitel and Deitel Fourth Edition, Chapter

 

Back to Csc 125 Programming in C++
  Scott Badman  Office:  B132  Phone:  353-2250  sbadman@parkland.edu  

Parkland College, 2400 W. Bradley Avenue, Champaign, IL 61821