Const only confuses library interfaces with the hope of catching some rare errors.
void f() {
const int n = 3;
int a[n]; // VLA in C99, but a[3] in c++
enum {e=n}; // invalid in c, ok in c++
switch(1) {
case n: ; // invalid in c, ok in c++
}
}
is valid c++)
In c const mostly matters only when used with pointers (the pointed object is not modified through the given pointer). (it can express that a non-pointer object is unmodified, but that's rarely useful, eg. in
void f(const int x) {
int y = x+1; // ok
x += 2; // error
}
the const may be used to ensure that x is not modified accidentally in the body of f.)
const int *x; // pointer to const int int const *y; // pointer to const int int *const z; // const pointer to int typedef int *intp; const intp w; // const pointer to intIt's easy to assume that x and w have compatible types, but actually z and w are the compatible ones.
char *strstr(const char *s1, const char *s2);
s1 is const (because the function does not modify the pointed object through s1), but the returned pointer is not const to make it assignable to non-const pointers (so the constness must be casted away internally).
Another similar case is the endptr in
long int strtol(const char *restrict nptr, char **restrict endptr, int base);If nptr points to const data then one would expect that *endptr points to const data as well, but again with a "proper" prototype that uses const consistently the function would be harder to use (because the caller may want to modify the string through endptr). This case is worse than the strstr one because while "char*" can be assigned to "const char*", "const char**" cannot be passed in place of "char**":
const char *s = "123"; char *t; long x = strtol(s, &s, 10); // bad: const char** is not compatible with char** long y = strtol(s, (char**)&s, 10); // not always ok long z = strtol(s, (void*)&s, 10); // not always ok long u = strtol(s, &(char*)s, 10); // not valid c long v = strtol(s, &t, 10); // ok s = t;
The y and z line may need explanation: the c standard does not guarantee that two pointer types have the same alignment and representation, only if they point to "qualified or unqualified versions of compatible types", so "char*" and "const char*" have the same alignment and representation, but "char**" and "const char**" may not, in which case the conversion is undefined behaviour (even if it goes through void* to which every object pointer can be converted).
// some data structure with a getter
struct s { ... };
int getx(const struct s *s) { ... return x; }
If later (as an implementation detail) caching or some kind of instrumentation is added to the function that otherwise does not modify its argument then the constness (hence the API) must be changed in a way that's incompatible with the original prototype. (Or the constness must be casted away internally.)
// adding statistics and memoization
struct s { ... int cachedx; int counter; };
int getx(struct s *s) { s->counter++; if(s->cachedx) return s->cachedx; ... s->cachedx = x; return x; }
void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
Assume we have a comparator that changes its arguments like in the previous caching example. Then we have to lie in the prototype of the comparator (and cast away the constness internally), casting the function pointer when qsort is invoked is not correct:
// correct prototype
int cmp(struct s *x, struct s *y) { ... }
// not ok
qsort(a, n, sizeof *a, (int (*)(const void *, const void *))cmp);
// ok
static int cmpwrap(const void *x, const void *y) { return cmp((struct s*)x, (struct s*)y); }
qsort(a, n, sizeof *a, cmpwrap);
// ok
static int cmpwrap2(const void *x, const void *y) { return cmp((void*)x, (void*)y); }
qsort(a, n, sizeof *a, cmpwrap2);
const T *cp; T *p; int i; p = (T*)cp; // this is what we want p = (T*)i; // accepted (bad) p = (T*)&i; // accepted (bad) p = (T*)3; // accepted (bad) p = (T*)3.14; // error (good)So a type cast can mask a whole range of type errors (maybe more than what the const qualifier protects against), so it's not always clear that using const actually makes things safer with respect to static type-checking.
const cannot protect against accidental writes to read-only memory. It does not protect against modifying a string literal through a char* pointer or dereferencing the null pointer. const cannot express that only a subrange of an array or some specific members of a struct may be modified through a given pointer.
TODO: incompatibility of const T** vs T** can easily lead to UB