|
| Tuesday, November 7 and Thursday, November 9, 2006 |
| const Correctness |
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 |