const
QualifierSometimes we want to define a variable whose value we know cannot be changed. For example, we might want to use a variable to refer to the size of a buffer size. Using a variable makes it easy for us to change the size of the buffer if we decided the original size wasn’t what we needed. On the other hand, we’d also like to prevent code from inadvertently giving a new value to the variable we use to represent the buffer size. We can make a variable unchangeable by defining the variable’s type as const
:
const int bufSize = 512; // input buffer size
defines bufSize
as a constant. Any attempt to assign to bufSize
is an error:
bufSize = 512; // error: attempt to write to const object
Because we can’t change the value of a const
object after we create it, it must be initialized. As usual, the initializer may be an arbitrarily complicated expression:
const int i = get_size(); // ok: initialized at run time
const int j = 42; // ok: initialized at compile time
const int k; // error: k is uninitialized const
const
As we have observed many times, the type of an object defines the operations that can be performed by that object. A const
type can use most but not all of the same operations as its nonconst
version. The one restriction is that we may use only those operations that cannot change an object. So, for example, we can use a const int
in arithmetic expressions in exactly the same way as a plain, nonconst int
. A const int
converts to bool
the same way as a plain int
, and so on.
Among the operations that don’t change the value of an object is initialization—when we use an object to initialize another object, it doesn’t matter whether either or both of the objects are const
s:
int i = 42;
const int ci = i; // ok: the value in i is copied into ci
int j = ci; // ok: the value in ci is copied into j
Although ci
is a const int
, the value in ci
is an int
. The const
ness of ci
matters only for operations that might change ci
. When we copy ci
to initialize j
, we don’t care that ci
is a const
. Copying an object doesn’t change that object. Once the copy is made, the new object has no further access to the original object.
const
Objects Are Local to a FileWhen a const
object is initialized from a compile-time constant, such as in our definition of bufSize
:
const int bufSize = 512; // input buffer size
the compiler will usually replace uses of the variable with its corresponding value during compilation. That is, the compiler will generate code using the value 512
in the places that our code uses bufSize
.
To substitute the value for the variable, the compiler has to see the variable’s initializer. When we split a program into multiple files, every file that uses the const
must have access to its initializer. In order to see the initializer, the variable must be defined in every file that wants to use the variable’s value (§ 2.2.2, p. 45). To support this usage, yet avoid multiple definitions of the same variable, const
variables are defined as local to the file. When we define a const
with the same name in multiple files, it is as if we had written definitions for separate variables in each file.
Sometimes we have a const
variable that we want to share across multiple files but whose initializer is not a constant expression. In this case, we don’t want the compiler to generate a separate variable in each file. Instead, we want the const
object to behave like other (nonconst
) variables. We want to define the const
in one file, and declare it in the other files that use that object.
To define a single instance of a const
variable, we use the keyword extern
on both its definition and declaration(s):
// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc
In this program, file_1.cc
defines and initializes bufSize
. Because this declaration includes an initializer, it is (as usual) a definition. However, because bufSize
is const
, we must specify extern
in order for bufSize
to be used in other files.
The declaration in file_1.h
is also extern
. In this case, the extern
signifies that bufSize
is not local to this file and that its definition will occur elsewhere.
Exercises Section 2.4
Exercise 2.26: Which of the following are legal? For those that are illegal, explain why.
(a)
const int buf;
(b)
int cnt = 0;
(c)
const int sz = cnt;
(d)
++cnt; ++sz;
const
As with any other object, we can bind a reference to an object of a const
type. To do so we use a reference to const
, which is a reference that refers to a const
type. Unlike an ordinary reference, a reference to const
cannot be used to change the object to which the reference is bound:
const int ci = 1024;
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: non const reference to a const object
Because we cannot assign directly to ci
, we also should not be able to use a reference to change ci
. Therefore, the initialization of r2
is an error. If this initialization were legal, we could use r2
to change the value of its underlying object.
C++ programmers tend to abbreviate the phrase “reference to
const
” as “const
reference.” This abbreviation makes sense—if you remember that it is an abbreviation.Technically speaking, there are no
const
references. A reference is not an object, so we cannot make a reference itselfconst
. Indeed, because there is no way to make a reference refer to a different object, in some sense all references areconst
. Whether a reference refers to aconst
or nonconst
type affects what we can do with that reference, not whether we can alter the binding of the reference itself.
const
In § 2.3.1 (p. 51) we noted that there are two exceptions to the rule that the type of a reference must match the type of the object to which it refers. The first exception is that we can initialize a reference to const
from any expression that can be converted (§ 2.1.2, p. 35) to the type of the reference. In particular, we can bind a reference to const
to a nonconst
object, a literal, or a more general expression:
int i = 42;
const int &r1 = i; // we can bind a const int& to a plain int object
const int &r2 = 42; // ok: r1 is a reference to const
const int &r3 = r1 * 2; // ok: r3 is a reference to const
int &r4 = r * 2; // error: r4 is a plain, non const reference
The easiest way to understand this difference in initialization rules is to consider what happens when we bind a reference to an object of a different type:
double dval = 3.14;
const int &ri = dval;
Here ri
refers to an int
. Operations on ri
will be integer operations, but dval
is a floating-point number, not an integer. To ensure that the object to which ri
is bound is an int
, the compiler transforms this code into something like
const int temp = dval; // create a temporary const int from the double
const int &ri = temp; // bind ri to that temporary
In this case, ri
is bound to a temporary object. A temporary object is an unnamed object created by the compiler when it needs a place to store a result from evaluating an expression. C++ programmers often use the word temporary as an abbreviation for temporary object.
Now consider what could happen if this initialization were allowed but ri
was not const
. If ri
weren’t const
, we could assign to ri
. Doing so would change the object to which ri
is bound. That object is a temporary, not dval
. The programmer who made ri
refer to dval
would probably expect that assigning to ri
would change dval
. After all, why assign to ri
unless the intent is to change the object to which ri
is bound? Because binding a reference to a temporary is almost surely not what the programmer intended, the language makes it illegal.
const
May Refer to an Object That Is Not const
It is important to realize that a reference to const
restricts only what we can do through that reference. Binding a reference to const
to an object says nothing about whether the underlying object itself is const
. Because the underlying object might be nonconst
, it might be changed by other means:
int i = 42;
int &r1 = i; // r1 bound to i
const int &r2 = i; // r2 also bound to i; but cannot be used to change i
r1 = 0; // r1 is not const; i is now 0
r2 = 0; // error: r2 is a reference to const
Binding r2
to the (nonconst
) int i
is legal. However, we cannot use r2
to change i
. Even so, the value in i
still might change. We can change i
by assigning to it directly, or by assigning to another reference bound to i
, such as r1
.
const
As with references, we can define pointers that point to either const
or nonconst
types. Like a reference to const
, a pointer to const
(§ 2.4.1, p. 61) may not be used to change the object to which the pointer points. We may store the address of a const
object only in a pointer to const
:
const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
In § 2.3.2 (p. 52) we noted that there are two exceptions to the rule that the types of a pointer and the object to which it points must match. The first exception is that we can use a pointer to const
to point to a nonconst
object:
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval; // ok: but can't change dval through cptr
Like a reference to const
, a pointer to const
says nothing about whether the object to which the pointer points is const
. Defining a pointer as a pointer to const
affects only what we can do with the pointer. It is important to remember that there is no guarantee that an object pointed to by a pointer to const
won’t change.
It may be helpful to think of pointers and references to
const
as pointers or references “that think they point or refer toconst
.”
const
PointersUnlike references, pointers are objects. Hence, as with any other object type, we can have a pointer that is itself const
. Like any other const
object, a const
pointer must be initialized, and once initialized, its value (i.e., the address that it holds) may not be changed. We indicate that the pointer is const
by putting the const
after the *
. This placement indicates that it is the pointer, not the pointed-to type, that is const
:
int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = π // pip is a const pointer to a const object
As we saw in § 2.3.3 (p. 58), the easiest way to understand these declarations is to read them from right to left. In this case, the symbol closest to curErr
is const
, which means that curErr
itself will be a const
object. The type of that object is formed from the rest of the declarator. The next symbol in the declarator is *
, which means that curErr
is a const
pointer. Finally, the base type of the declaration completes the type of curErr
, which is a const
pointer to an object of type int
. Similarly, pip
is a const
pointer to an object of type const double
.
The fact that a pointer is itself const
says nothing about whether we can use the pointer to change the underlying object. Whether we can change that object depends entirely on the type to which the pointer points. For example, pip
is a const
pointer to const
. Neither the value of the object addressed by pip
nor the address stored in pip
can be changed. On the other hand, curErr
addresses a plain, nonconst int
. We can use curErr
to change the value of errNumb
:
*pip = 2.72; // error: pip is a pointer to const
// if the object to which curErr points (i.e., errNumb) is nonzero
if (*curErr) {
errorHandler();
*curErr = 0; // ok: reset the value of the object to which curErr is bound
}
const
As we’ve seen, a pointer is an object that can point to a different object. As a result, we can talk independently about whether a pointer is const
and whether the objects to which it can point are const
. We use the term top-level const
to indicate that the pointer itself is a const
. When a pointer can point to a const
object, we refer to that const
as a low-level const
.
Exercises Section 2.4.2
Exercise 2.27: Which of the following initializations are legal? Explain why.
(a)
int i = -1, &r = 0;
(b)
int *const p2 = &i2;
(c)
const int i = -1, &r = 0;
(d)
const int *const p3 = &i2;
(e)
const int *p1 = &i2;
(f)
const int &const r2;
(g)
const int i2 = i, &r = i;
Exercise 2.28: Explain the following definitions. Identify any that are illegal.
(a)
int i, *const cp;
(b)
int *p1, *const p2;
(c)
const int ic, &r = ic;
(d)
const int *const p3;
(e)
const int *p;
Exercise 2.29: Uing the variables in the previous exercise, which of the following assignments are legal? Explain why.
(a)
i = ic;
(b)
p1 = p3;
(c)
p1 = ⁣
(d)
p3 = ⁣
(e)
p2 = p1;
(f)
ic = *p3;
More generally, top-level const
indicates that an object itself is const
. Top-level const
can appear in any object type, i.e., one of the built-in arithmetic types, a class type, or a pointer type. Low-level const
appears in the base type of compound types such as pointers or references. Note that pointer types, unlike most other types, can have both top-level and low-level const
independently:
int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int ci = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level
The distinction between top-level and low-level matters when we copy an object. When we copy an object, top-level const
s are ignored:
i = ci; // ok: copying the value of ci; top-level const in ci is ignored
p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored
Copying an object doesn’t change the copied object. As a result, it is immaterial whether the object copied from or copied into is const
.
On the other hand, low-level const
is never ignored. When we copy an object, both objects must have the same low-level const
qualification or there must be a conversion between the types of the two objects. In general, we can convert a nonconst
to const
but not the other way round:
int *p = p3; // error: p3 has a low-level const but p doesn't
p2 = p3; // ok: p2 has the same low-level const qualification as p3
p2 = &i; // ok: we can convert int* to const int*
int &r = ci; // error: can't bind an ordinary int& to a const int object
const int &r2 = i; // ok: can bind const int& to plain int
p3
has both a top-level and low-level const
. When we copy p3
, we can ignore its top-level const
but not the fact that it points to a const
type. Hence, we cannot use p3
to initialize p
, which points to a plain (nonconst
) int
. On the other hand, we can assign p3
to p2
. Both pointers have the same (low-level const
) type. The fact that p3
is a const
pointer (i.e., that it has a top-level const
) doesn’t matter.
Exercises Section 2.4.3
Exercise 2.30: For each of the following declarations indicate whether the object being declared has top-level or low-level
const
.const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;Exercise 2.31: Given the declarations in the previous exercise determine whether the following assignments are legal. Explain how the top-level or low-level
const
applies in each case.r1 = v2;
p1 = p2; p2 = p1;
p1 = p3; p2 = p3;
constexpr
and Constant ExpressionsA constant expression is an expression whose value cannot change and that can be evaluated at compile time. A literal is a constant expression. A const
object that is initialized from a constant expression is also a constant expression. As we’ll see, there are several contexts in the language that require constant expressions.
Whether a given object (or expression) is a constant expression depends on the types and the initializers. For example:
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression
Although staff_size
is initialized from a literal, it is not a constant expression because it is a plain int
, not a const int
. On the other hand, even though sz
is a const
, the value of its initializer is not known until run time. Hence, sz
is not a constant expression.
constexpr
VariablesIn a large system, it can be difficult to determine (for certain) that an initializer is a constant expression. We might define a const
variable with an initializer that we think is a constant expression. However, when we use that variable in a context that requires a constant expression we may discover that the initializer was not a constant expression. In general, the definition of an object and its use in such a context can be widely separated.
Under the new standard, we can ask the compiler to verify that a variable is a constant expression by declaring the variable in a constexpr
declaration. Variables declared as constexpr
are implicitly const
and must be initialized by constant expressions:
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
Although we cannot use an ordinary function as an initializer for a constexpr
variable, we’ll see in § 6.5.2 (p. 239) that the new standard lets us define certain functions as constexpr
. Such functions must be simple enough that the compiler can evaluate them at compile time. We can use constexpr
functions in the initializer of a constexpr
variable.
Generally, it is a good idea to use
constexpr
for variables that you intend to use as constant expressions.
Because a constant expression is one that can be evaluated at compile time, there are limits on the types that we can use in a constexpr
declaration. The types we can use in a constexpr
are known as “literal types” because they are simple enough to have literal values.
Of the types we have used so far, the arithmetic, reference, and pointer types are literal types. Our Sales_item
class and the library IO and string
types are not literal types. Hence, we cannot define variables of these types as constexpr
s. We’ll see other kinds of literal types in § 7.5.6 (p. 299) and § 19.3 (p. 832).
Although we can define both pointers and reference as constexpr
s, the objects we use to initialize them are strictly limited. We can initialize a constexpr
pointer from the nullptr
literal or the literal (i.e., constant expression) 0
. We can also point to (or bind to) an object that remains at a fixed address.
For reasons we’ll cover in § 6.1.1 (p. 204), variables defined inside a function ordinarily are not stored at a fixed address. Hence, we cannot use a constexpr
pointer to point to such variables. On the other hand, the address of an object defined outside of any function is a constant expression, and so may be used to initialize a constexpr
pointer. We’ll see in § 6.1.1 (p. 205), that functions may define variables that exist across calls to that function. Like an object defined outside any function, these special local objects also have fixed addresses. Therefore, a constexpr
reference may be bound to, and a constexpr
pointer may address, such variables.
constexpr
It is important to understand that when we define a pointer in a constexpr
declaration, the constexpr
specifier applies to the pointer, not the type to which the pointer points:
const int *p = nullptr; // p is a pointer to a const int
constexpr int *q = nullptr; // q is a const pointer to int
Despite appearances, the types of p
and q
are quite different; p
is a pointer to const
, whereas q
is a constant pointer. The difference is a consequence of the fact that constexpr
imposes a top-level const
(§ 2.4.3, p. 63) on the objects it defines.
Like any other constant pointer, a constexpr
pointer may point to a const
or a nonconst
type:
constexpr int *np = nullptr; // np is a constant pointer to int that is null
int j = 0;
constexpr int i = 42; // type of i is const int
// i and j must be defined outside any function
constexpr const int *p = &i; // p is a constant pointer to the const int i
constexpr int *p1 = &j; // p1 is a constant pointer to the int j
Exercises Section 2.4.4
Exercise 2.32: Is the following code legal or not? If not, how might you make it legal?
int null = 0, *p = null;