Names specified here
Name Description Notes Source Availability
const Marks unassignable object L Q Keyword C89 C90 C95 C99 C11
restrict Marks unshared memory L Q Keyword C99 C11
volatile Marks unoptimizable object L Q Keyword C89 C90 C95 C99 C11

Type qualifiers are a set of keywords that provide more detail about a type:

type-qualifier
const
restrict
volatile
_Atomic
type-qualifier-list
type-qualifier
type-qualifier-list type-qualifier
declaration-specifiers
type-qualifier declaration-specifiersopt
specifier-qualifier-list
type-qualifier specifier-qualifier-listopt
pointer
* type-qualifier-listopt
* type-qualifier-listopt pointer
direct-declarator
direct-declarator [ type-qualifier-listopt assignment-expressionopt ]
direct-declarator [ static type-qualifier-listopt assignment-expression ]
direct-declarator [ type-qualifier-list static assignment-expression ]
direct-declarator [ type-qualifier-listopt * ]
direct-abstract-declarator
direct-abstract-declaratoropt [ type-qualifier-listopt assignment-expressionopt ]
direct-abstract-declaratoropt [ static type-qualifier-listopt assignment-expression ]
direct-abstract-declaratoropt [ type-qualifier-list static assignment-expression ]

Type qualification with const

When const appears in a type, it indicates that the compiler will forbid attempts to modify objects of that type.

const int lim = 5; // or
int const lim = 5;

int a = lim + 7;
lim++;    // error
lim = 10; // error

Such constant objects normally have to be initialized when defined, since they can't subsequently take on a value later. However, this does not apply to function parameters, which are initialized on each function call:

void func(const int lim)
{
  . . .
}

This doesn't mean that lim always has the same value on each invocation because, as an object with automatic storage, it is not really a single variable, but a template for creating a variable on each invocation of the function. This use might help to discipline a programmer in implementing the function, but it is of little consequence to the caller, however, because the function already has no means to affect variables provided by the caller as arguments.

A more interesting use of const is with pointers. When taking the address of an object, the qualifiers of the object's type are preserved in the pointer type. For example:

const int lim = 5;

The address of lim is of type const int *, not merely int *. It is an error to assign a value of type const T * to a variable of type T *, as the value then stored in the variable could be used to modify the object it points to. Consider this function, which modifies what its argument points to:

void increment(int *ptr)
{
  ++*ptr;
}

const int lim = 5;
increment(&lim); // error

This would undermine the supposition that other users of lim have, i.e., that it is immutable.

In contrast, it is possible to assign a value of type T * to a variable of type const T *. Functions that take strings as arguments, but don't need to modify them, still have to be declared as taking a pointer to the start of the string, which enables them to modify it. strlen could have been defined as follows:

size_t strlen(char *in)
{
  char *orig = in;
  while (*in++)
    ;
  return in - orig;
}

Of course, the corresponding prototype would then be:

size_t strlen(char *);

The caller, who does not know how the function is implemented, and only sees its prototype, must trust that the function will not mistakenly modify the string. A small modification helps to assure the caller, and helps the implementor to check that he has not inadvertently broken his side of the contract not to modify the string:

size_t strlen(const char *in)
{
  char *orig = in;
  while (*in++)
    ;
  return in - orig;
}

Now it can be used with unmodifiable strings like msg:

const char msg[] = "foo";
char buf[100];

strlen(msg); // okay
strlen(buf); // okay

It is also safe to use with modifiable arrays like buf, whose type decays to char *, which is safe to assign to const char *.

Type qualification with volatile

As part of a type, volatile tells the compiler that an object's value might change unexpectedly. It is sometimes used when writing low-level device drivers that access memory-mapped I/O, or in writing signal handlers or multithreaded programs. Ordinarily, the compiler may assume that an object won't be modified by code other than what the compiler is currently working on, and so it then could perform optimizations based on that assumption. Consider this:

int counter;

printf("%d\n", counter);
printf("%d\n", counter);

This might be translated such that the memory holding the value of counter is read only once, since the compiler knows that nothing will change the value between the two apparent reads of counter. In contrast:

volatile int counter;

printf("%d\n", counter);
printf("%d\n", counter);

The compiler will now disable that optimization, and cause the the memory to be read twice.

Another optimization assumes that nothing else reads from a variable. In this code:

int counter;

counter = 10;
counter = 20;

…the compiler may choose to ignore the first assignment, since its value is discarded by the second assignment, and nothing uses the first value in between.

volatile int counter;

counter = 10;
counter = 20;

By introducing volatile, the compiler can no longer make this assumption, and will translate the two writes.

Type qualification with restrict

When two restrict-qualified pointers are in scope, such as when they are both function parameters, the compiler may assume that they point to distinct objects, i.e., they do not overlap, allowing certain kinds of optimization to be applied. Consider the implementation of this function:

void func(int *p1,
          int *p2,
          const int *pinc)
{
  *p1 += *pinc;
  *p2 += *pinc;
}

Suppose that p1 == pinc, e.g., by calling func(&c1, &c2, &c1). The first statement will produce an increase in *pinc, which means that the second statement will increment by a greater amount than expected. The compiler must assume that this is possible, even if it's not likely, so it must re-read *pinc to ensure it's using the correct increment. In contrast:

void func(int *restrict p1,
          int *restrict p2,
          const int *restrict pinc)
{
  *p1 += *pinc;
  *p2 += *pinc;
}

The programmer is now telling the compiler that the three referenced objects can be assumed to be distinct. It may assume that altering *p1 has no effect on *pinc, so it can optimize away the re-reading of *pinc to perform the second statement.

[ Work in progress : Isn't there a better example? For this one, we could just create an extra local variable to record the initial value of *pinc. There must be some deeper justification.]

A prototype for a function defined using restrict does not have to include the keyword, so the following is compatible with the example:

void func(int *p1,
          int *p2,
          const int *pinc);

restrict doesn't formally affect the caller of the function, not least because it is extremely hard, if not impossible in most cases, to check whether supplied arguments violate the non-overlapping assumption. If they do, the behaviour is undefined. Instead, the presence of restrict in a prototype can at best serve as informal documentation.

Most of the functions in the Standard Library that take at least two pointer arguments are specified with restrict on both parameters. However, since the compiler probably can't enforce the qualification on the users of these functions, restrict has been omitted from declarations that appear on this site. You should assume that all functions that take two or more pointer arguments are only guaranteed to work if the referenced objects don't overlap, unless the documentation for the function explicitly states that overlaps are tolerated. Two such functions are memmove and wmemmove, in contrast to their otherwise identical counterparts, memcpy and wmemcpy.

The example is based on the Wikipedia entry for restrict.


CHaR
Sitemap Supported
Site format updated 2024-06-05T22:37:07.391+0000
Data updated 1970-01-01T00:00:00.000+0000
Page updated 2022-06-17T21:43:05.000+0000