const
Parameters and ArgumentsWhen we use parameters that are const
, it is important to remember the discussion of top-level const
from § 2.4.3 (p. 63). As we saw in that section, a top-level const
is one that applies to the object itself:
const int ci = 42; // we cannot change ci; const is top-level
int i = ci; // ok: when we copy ci, its top-level const is ignored
int * const p = &i; // const is top-level; we can't assign to p
*p = 0; // ok: changes through p are allowed; i is now 0
Just as in any other initialization, when we copy an argument to initialize a parameter, top-level const
s are ignored. As a result, top-level const
on parameters are ignored. We can pass either a const
or a nonconst
object to a parameter that has a top-level const
:
void fcn(const int i) { /* fcn can read but not write to i */ }
We can call fcn
passing it either a const int
or a plain int
. The fact that top-level const
s are ignored on a parameter has one possibly surprising implication:
void fcn(const int i) { /* fcn can read but not write to i */ }
void fcn(int i) { /* . . . */ } // error: redefines fcn(int)
In C++, we can define several different functions that have the same name. However, we can do so only if their parameter lists are sufficiently different. Because top-level const
s are ignored, we can pass exactly the same types to either version of fcn
. The second version of fcn
is an error. Despite appearances, its parameter list doesn’t differ from the list in the first version of fcn
.
const
Because parameters are initialized in the same way that variables are initialized, it can be helpful to remember the general initialization rules. We can initialize an object with a low-level const
from a nonconst
object but not vice versa, and a plain reference must be initialized from an object of the same type.
int i = 42;
const int *cp = &i; // ok: but cp can't change i (§ 2.4.2 (p. 62))
const int &r = i; // ok: but r can't change i (§ 2.4.1 (p. 61))
const int &r2 = 42; // ok: (§ 2.4.1 (p. 61))
int *p = cp; // error: types of p and cp don't match (§ 2.4.2 (p. 62))
int &r3 = r; // error: types of r3 and r don't match (§ 2.4.1 (p. 61))
int &r4 = 42; // error: can't initialize a plain reference from a literal (§ 2.3.1 (p. 50))
Exactly the same initialization rules apply to parameter passing:
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i); // calls the version of reset that has an int* parameter
reset(&ci); // error: can't initialize an int* from a pointer to a const int object
reset(i); // calls the version of reset that has an int& parameter
reset(ci); // error: can't bind a plain reference to the const object ci
reset(42); // error: can't bind a plain reference to a literal
reset(ctr); // error: types don't match; ctr has an unsigned type
// ok: find_char's first parameter is a reference to const
find_char("Hello World!", 'o', ctr);
We can call the reference version of reset
(§ 6.2.2, p. 210) only on int
objects. We cannot pass a literal, an expression that evaluates to an int
, an object that requires conversion, or a const int
object. Similarly, we may pass only an int*
to the pointer version of reset
(§ 6.2.1, p. 209). On the other hand, we can pass a string literal as the first argument to find_char
(§ 6.2.2, p. 211). That function’s reference parameter is a reference to const
, and we can initialize references to const
from literals.
const
When PossibleIt is a somewhat common mistake to define parameters that a function does not change as (plain) references. Doing so gives the function’s caller the misleading impression that the function might change its argument’s value. Moreover, using a reference instead of a reference to const
unduly limits the type of arguments that can be used with the function. As we’ve just seen, we cannot pass a const
object, or a literal, or an object that requires conversion to a plain reference parameter.
The effect of this mistake can be surprisingly pervasive. As an example, consider our find_char
function from § 6.2.2 (p. 211). That function (correctly) made its string
parameter a reference to const
. Had we defined that parameter as a plain string&
:
// bad design: the first parameter should be a const string&
string::size_type find_char(string &s, char c,
string::size_type &occurs);
we could call find_char
only on a string
object. A call such as
find_char("Hello World", 'o', ctr);
would fail at compile time.
More subtly, we could not use this version of find_char
from other functions that (correctly) define their parameters as references to const
. For example, we might want to use find_char
inside a function that determines whether a string
represents a sentence:
bool is_sentence(const string &s)
{
// if there's a single period at the end of s, then s is a sentence
string::size_type ctr = 0;
return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1;
}
If find_char
took a plain string&
, then this call to find_char
would be a compile-time error. The problem is that s
is a reference to a const string
, but find_char
was (incorrectly) defined to take a plain reference.
It might be tempting to try to fix this problem by changing the type of the parameter in is_sentence
. But that fix only propagates the error—callers of is_sentence
could pass only nonconst string
s.
The right way to fix this problem is to fix the parameter in find_char
. If it’s not possible to change find_char
, then define a local string
copy of s
inside is_sentence
and pass that string
to find_char
.