Type Qualifiers: const and restrict

When describing data, the C language offers data types and data qualifiers. The data type is well known to any C programmer, defining the kind of data stored: char, int, float, and so on. The qualifier describes additional aspects of the data, such as how it’s used or whether the compiler should optimize the data’s storage.

Here are the data qualifier keywords in the C language:

const
restrict
volatile
_Atomic

These differ from storage class specifiers: auto, register, static, extern, and typedef. In a way, these declarations tell the compiler how to store the data. The data qualifiers tell the compiler how the data is to be treated and suggest optimizations. My descriptions are general and subject to ire of sophomore computer science students, so I can be accused of over-simplification here.

I cover the const keyword in my books and online courses. It ensures that the data declared isn’t modified in the code. Its scope is within the function where the declaration is made. Because the constant value can’t be modified, it’s assigned a value in its declaration:

const int full = 100;

Above, integer full is set to 100 and made constant. This variable is used throughout the function, but never altered. If an attempt is made, the compiler shrieks in horror.

The const data qualifier differs from the defined constant in scope. The precompiler DEFINE directive creates a constant visible throughout the source code. But a defined constant value is replaced as the program is built. On the other hand, a const value is assigned storage like any other variable and used that way in the program.

Beyond const, my bet is that only a few C programmers use the restrict, volatile, and _Atomic qualifiers. From my research, these specifiers are (no surprise) quite specific in how they modify data and when their use is necessary.

You find the restrict specifier used in various function declarations in the man pages:

int printf(const char * restrict format, ...);
char *fgets(char * restrict str, int size, FILE * restrict stream);
FILE *fopen(const char * restrict path, const char * restrict mode);

In each declaration, see how restrict is associated with a pointer? In a way, think of a restrict specifier as a promise made to the compiler that no other pointer is ever used to access the same data chunk. This restriction (get it?) also implies that each string passed to a function is unique.

The restrict specification is made primarily for compiler optimization. By ensuring that only one pointer references a chunk of data, the compiler is able to reduce overhead when accessing the data. I’m unsure of the mechanics, and though sample code is available to show restrict in action, it’s difficult to visually demonstrate how this optimization works or how it’s effective.

Next week I ruminate on the volatile keyword.

3 thoughts on “Type Qualifiers: const and restrict

  1. When I first encountered C99ʼs “restrict” keyword, it wasnʼt—to me—really obvious either, why this keyword should be necessary… until I read David H. Bartleyʼs excellent »Performance Tuning with the “Restrict”¹ Keyword« that is – on page 10 of said whitepaper, the usefulness of “restrict” is explained by the following example:

    a = *p1; // Is it safe to load *p1 into a register, e.g. R1 = *p1,
    *p2 = b;
    c = *p1 + 5; //…and use R1ʼs value here, or is it necessary to re-load *p1 again?

    … if the compiler is able to prove to itself, that pointers p1 and p2 do not point to the same chunk of memory (“are not aliased”), it may load *p1 into a register and re-use it for the assignment to ‘c’. Otherwise, *p1 will need to be read from memory again.

    Admittedly, as illustrated on page 20, the given description isnʼt completely accurate either. Thatʼs because itʼs not really necessary for pointers to reference entirely different chunks of memory – “restrict” only promises, that “writes to” and “reads from” memory (through different pointers) wonʼt overlap:

    /* void *memcpy (void *restrict dest, void const *restrict src, size_t n); */
    int arr [100];
    memcpy (&arr [50], &arr [0], 50*sizeof(arr[0])); // Valid, no overlapping accesses.
    memcpy (&arr [1], &arr [0], 50*sizeof(arr[0])); // Overlap, undefined behavior.

    If anyone is interested in the nitty-gritty details, I recommend taking a look at the linked whitepaper!

    ¹ Originally published on Texas Instruments »Processors Wiki«, itʼs – sadly – no longer available online since it has been removed by TI on 2021-01-15; a copy of this whitepaper can be found be found here: https://tinyurl.com/2zvnt3ek

  2. Pretty cool. One of the reasons I took to pointers in C was my experience coding assembly. It makes more sense from that direction.

  3. I agree. “C” didn’t used to be called “figh-level assembler” for no reason… to understand certain finer points, knowledge of (at least some dialect of) assembly language might not be a strict requirement but is an advantage for sure.

Leave a Reply