C Const Conundrum

Paul J. Lucas - Jan 30 - - Dev Community

Introduction

Originally, const was not part of C. (C++ added const and it was later back-ported to C89.) In C, there are two uses for const:

  1. const objects.
  2. Pointers to const.

Neither is particularly complicated. However, constexpr was added in C23. Knowing which of the two to use in a particular case can be quite the conundrum.

If this article seems like déjà vu, it’s because I previously wrote about a C++ Const Conundrum. However, this article is about C.

const

As a refresher, here are details for the use of const in C. There are two parts to “const-ness”:

  1. Whether it’s initialized at compile-time (vs. run-time) — constant initialization.

  2. It’s immutable after being initialized — immutability.

An object declared const:

  • Always means #2 above: it’s immutable.

  • Must be explicitly initialized in its definition.

  • May be initialized with a constant (compile-time) expression.

  • However, unlike C++, objects with static storage duration (those either at global scope or explicitly declared static), must be initialized with a constant expression.

  • Objects with dynamic storage duration (non-static, function-local objects) may be initialized with a dynamic (run-time) expression.

  • For global objects, is extern (has external linkage) by default.

  • May be explicitly declared static (have internal linkage).

C++ also has const objects, except in C++, const for global objects is static by default. If compatibility with C++ is required or you just want to be clear and not have to remember what the default linkage is in which language, always explicitly specify either static or extern.

For example:

// file.h
extern int const MAX;       // Declaration.

// file.cpp
int const MAX = 10;         // Definition.

int const ANSWER = 42;      // Initialized at compile-time.
int const SEED   = rand();  // Error in C; OK in C++.

int main() {
  int const SEED = rand();  // OK in both C and C++.
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Note that the compiler doesn’t care where you put const:

const int WEST = 180;
int const EAST = 0;         // Same as: const int.
Enter fullscreen mode Exit fullscreen mode

The latter style is known as east const and it’s the style I prefer because then const is always making constant what’s to its immediate left (hence, the const is to the right or “east”). (This becomes relevant when pointers are involved.)

Attempting to modify a constant object results in undefined behavior:

int *p = (int*)&MAX;
*p = 43;                    // Undefined behavior.
Enter fullscreen mode Exit fullscreen mode

Pointers to const

A pointer to const can point to either a const or non-const object. The const means that you can’t modify the pointed-to object via that pointer. Whether the pointed-to object is actually const is irrelevant:

int i = 42;
int const *p = &i;  // pointer to const pointing at non-const
*p = 43;            // error: can't modify via pointer to const
Enter fullscreen mode Exit fullscreen mode

Note that the pointer itself can also be const:

int *const cp = &i;         // constant pointer to non-const
int const *const cpc = &i;  // constant pointer to const
Enter fullscreen mode Exit fullscreen mode

For a constant pointer, the const must appear to the right (“east”) of the *.

You’ll often read or hear pointers to const referred to as “const pointers.” The problem is that “const pointer” technically means the pointer is const, not the pointed-to object. Therefore to be precise, I’ll use “pointer to const” unless I really mean “constant pointer.”

const Isn’t Quite Constant

You might think that anything declared const is, well, constant. The problem is that in some cases, the compiler treats const like it isn’t really constant.

The first case is that a const integer can not be used as a case label:

void f( int n ) {
  int const MAX = 10;
  switch ( n ) {
    case MAX:             // Error.
      // ...
Enter fullscreen mode Exit fullscreen mode

Fortunately, you don’t run into this case that often. However, the second, more common, and surprising case is that a const integer can not be used to specify an array’s size:

int const MAX = 10;
int array[MAX];           // Error.
Enter fullscreen mode Exit fullscreen mode

Note that some compilers will accept the above code as an extension.

Inside a function, such code will be accepted, but for a different reason:

void f( void ) {
  int const MAX = 10;
  int array[MAX];         // OK, but VLA.
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The code is accepted not because the compiler considers MAX to be constant, but because array is a variable length array — something you probably don’t want.

The traditional work-arounds for lack of true “const-ness” for const are either to use a macro or an enumeration:

#define MAX1 10
int array1[MAX1];         // OK.

enum { MAX2 = 10 };
int array2[MAX2];         // OK.
Enter fullscreen mode Exit fullscreen mode

Being forced to use either because const isn’t truly constant is just deficient. This is where constexpr comes in.

constexpr

C23 adopted constexpr from C++ that’s a “const-er” const for objects. An object declared constexpr, like const:

  • Must be explicitly initialized in its definition.

However, unlike const:

  • Is always initialized at compile-time.
  • Must be initialized with a constant expression.
  • May not be explicitly declared extern (external linkage).
  • Is itself a constant expression, i.e., is truly constant.

A constexpr object can be used in places where constant expressions are required such as the aforementioned case labels and array dimensions:

constexpr int MAX3 = 10;
int array3[MAX3];         // OK in C23.
Enter fullscreen mode Exit fullscreen mode

Why didn’t the C Committee just “fix” const instead, i.e., make it truly constant in all cases? Because they didn’t want to change the meaning of existing code nor increase the existing incompatibility of const between C and C++.

No constexpr Functions

In C++, constexpr can also be used for functions; however, that ability was not adopted into C. (Perhaps it will be in a future standard revision.)

Miscellaneous

Since a constexpr object is truly constant, adding const is pointless:

constexpr const int CECI = 42;  // const is pointless
Enter fullscreen mode Exit fullscreen mode

“Const Correctness”

You’ve likely heard the term const correctness. Historically, this involved only const. With the addition of constexpr, does the meaning of const correctness change?

Yes. Generally, constexpr should be preferred to const. Use const only when you need dynamic initialization.

Conclusion

With the adoption of constexpr for objects, C finally fixed a long-standing language deficiency. Use constexpr whenever possible.

References

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .