[an error occurred while processing this directive]

HP OpenVMS Systems

C Programming Language
Content starts here HP C

HP C
Language Reference Manual


Previous Contents Index

3.7 Type Qualifiers

There are four type qualifiers:

  • const
  • volatile
  • __unaligned (axp)
  • __restrict (pointer type only)

Type qualifiers were introduced by the ANSI C standard to, in part, give you greater control over the compiler's optimizations. The const and volatile type qualifiers can be applied to any type. The __restrict type qualifier can be applied only to pointer types.

Note that because the __restrict type qualifier is not part of the 1989 ANSI C standard, this keyword has double leading underscores. The next version (9X) of the C standard is expected to adopt the keyword restrict with the same semantics described in this section.

The use of const gives you a method of controlling write access to an object, and eliminates potential side effects across function calls involving that object. This is because a side effect is an alteration of an object's storage and const prohibits such alteration.

Use volatile to qualify an object that can be changed by other processes or hardware. The use of volatile disables optimizations with respect to referencing the object. If an object is volatile qualified, it may be changed between the time it is initialized and any subsequent assignments. Therefore, it cannot be optimized.

Function parameters, however, do not all share the type qualification of one parameter. For example:


int f( const int a, int b)   /*  a is const qualified; b is not  */ 

When using a type qualifier with an array identifier, the elements of the array are qualified, not the array type itself.

The following declarations and expressions show the behavior when type qualifiers modify an array or structure type:


const struct s { int mem; } cs = { 1 }; 
struct s ncs;                        /* ncs is modifiable         */ 
typedef int A[2][3]; 
const A a = {{4, 5, 6}, {7, 8, 9}};  /*  array of array of const  */ 
                                     /*  int's                    */ 
int *pi; 
const int *pci; 
 
ncs = cs;            /*  Valid                                    */ 
cs = ncs;            /*  Invalid, cs is const-qualified           */ 
pi = &ncs.mem;       /*  Valid                                    */ 
pi = &cs.mem;        /*  Violates type constraints for = operator */ 
pci = &cs.mem;       /*  Valid                                    */ 
pi = a[0];           /*  Invalid; a[0] has type "const int *"     */ 

3.7.1 const Type Qualifier

Use the const type qualifier to qualify an object whose value cannot be changed. Objects qualified by the const keyword cannot be modified. This means that an object declared as const cannot serve as the operand in an operation that changes its value; for example, the ++ and -- operators are not allowed on objects qualified with const . Using the const qualifier on an object protects it from the side effects caused by operations that alter storage.

The declaration of const -qualified objects can be slightly more complicated than that for nonqualified types. Here are some examples, with explanatory comments:


const int x = 44;   /*  const qualification of int type. 
                        The value of x cannot be modified. */  
const int *z;       /*  Pointer to a constant integer. 
                        The value in the location pointed 
                        to by z cannot be modified.        */ 
int * const ptr;    /*  A constant pointer, a pointer  
                        that will always point to the 
                        same location                      */ 
const int *const p; /*  A constant pointer to a constant 
                        integer: neither the pointer or 
                        the integer can be modified.       */ 
const const int y;  /*  Illegal - redundant use of const   */ 

The following rules apply to the const type qualifier:

  • The const qualifier can be used to qualify any data type, including a single member of a structure or union.
  • If const is specified when declaring an aggregate type, all members of the aggregate type are treated as objects qualified with const . When const is used to qualify a member of an aggregate type, only that member is qualified. For example:


    const struct employee { 
        char *name; 
        int   birthdate; /* name, birthdate, job_code, and salary are */ 
        int   job_code;  /* treated as though declared with const.    */ 
        float salary; 
        } a, b;          /* All members of a and b are const-qualified*/ 
    struct employee2 { 
        char *name; 
        const int birthdate;  /*  Only this member is qualified    */ 
        int job_code; 
        float salary; 
        } c, d; 
    

    All members in the previous structure are qualified with const . If the tag employee is used to specify another structure later in the program, the const qualifier does not apply to the new structure's members unless explicitly specified.
  • The const qualifier can be specified with the volatile qualifier. This is useful, for example, in a declaration of a data object that is immutable by the source process but can be changed by other processes, or as a model of a memory-mapped input port such as a real-time clock.
  • The address of a non- const object can be assigned to a pointer to a const object (with an explicit const specifier), but that pointer cannot be used to alter the value of the object. For example:


    const int i = 0; 
    int j = 1; 
    const int *p = &i; /* Explicit const specifier required      */ 
    int *q = &j; 
    *p = 1;            /* Error -- attempt to modify a const- 
                          qualified object through a pointer      */ 
    *q = 1;            /* OK                                     */ 
    
  • Attempting to modify a const object using a pointer to a non- const qualified type causes unpredictable behavior.

3.7.2 volatile Type Qualifier

Any object whose type includes the volatile type qualifier indicates that the object should not be subject to compiler optimizations altering references to, or modifications of, the object.

Note

volatile objects are especially prone to side effects. (See Section 2.5.)

Optimizations that are defeated by using the volatile specifier can be categorized as follows:

  • Optimizations that alter an object's duration; for example, cases where references to the object are shifted or moved to another part of the program.
  • Optimizations that alter an object's locality; for example, cases where a variable serving as a loop counter is stored in a register to save the cost of doing a memory reference.
  • Optimizations that alter an object's existence; for example, loop induction to actually eliminate a variable reference.

An object without the volatile specifier does not compel the compiler to perform these optimizations; it indicates that the compiler has the freedom to apply the optimizations depending on program context and compiler optimization level.

The volatile qualifier forces the compiler to allocate memory for the volatile object, and to always access the object from memory. This qualifier is often used to declare that an object can be accessed in some way not under the compiler's control. Therefore, an object qualified by the volatile keyword can be modified or accessed in ways by other processes or hardware, and is especially vulnerable to side effects.

The following rules apply to the use of the volatile qualifier:

  • The volatile qualifier can be used to qualify any data type, including a single member of a structure or union.
  • Redundant use of the volatile keyword elicits a warning message. For example:


    volatile volatile int x; 
    
  • When volatile is used with an aggregate type declaration, all members of the aggregate type are qualified with volatile . When volatile is used to qualify a member of an aggregate type, only that member is qualified. For example:


    volatile struct employee { 
        char *name; 
        int   birthdate; /* name, birthdate, job_code, and salary are */ 
        int   job_code;  /* treated as though declared with volatile. */ 
        float salary; 
        } a,b;          /*  All members of a and b are volatile-qualified  */ 
    struct employee2 { 
        char *name; 
        volatile int birthdate;  /*  Only this member is qualified    */ 
        int job_code; 
        float salary; 
        } c, d; 
    

    If the tag employee is used to specify another structure later in the program, the volatile qualifier does not apply to the new structure's members unless explicitly specified.
  • The const qualifier can be used with the volatile qualifier. This is useful, for example, in a declaration of a data object that is immutable by the source process but can be changed by other processes, or as a model of a memory-mapped input port such as a real-time clock.
  • The address of a non- volatile object can be assigned to a pointer that points to a volatile object. For example:


    const int *intptr; 
    volatile int x; 
    intptr = &x; 
    

    Likewise, the address of a volatile object can be assigned to a pointer that points to a non- volatile object.

3.7.3 __unaligned Type Qualifier

Use this data-type qualifier in pointer definitions to indicate to the compiler that the data pointed to is not properly aligned on a correct address. (To be properly aligned, the address of an object must be a multiple of the size of the type. For example, two-byte objects must be aligned on even addresses.)

When data is accessed through a pointer declared __unaligned , the compiler generates the additional code necessary to copy or store the data without causing alignment errors. It is best to avoid use of misaligned data altogether, but in some cases the usage may be justified by the need to access packed structures, or by other considerations.

Here is an example of a typical use of __unaligned :


typedef enum {int_kind, float_kind, double_kind} kind; 
void foo(void *ptr, kind k) { 
    switch (k) { 
    case int_kind: 
        printf("%d", *(__unaligned int *)ptr); 
        break; 
    case float_kind: 
        printf("%f", *(__unaligned float *)ptr); 
        break; 
    case double_kind: 
        printf("%f", *(__unaligned double *)ptr); 
        break; 
    } 
} 

3.7.4 __restrict Type Qualifier

Use the __restrict type qualifier on the declaration of a pointer type to indicate that the pointer is subject to compiler optimizations. Restricted pointers are expected to be an addition to the 9X revision of the ISO C Standard. Using restricted pointers judiciously can often improve the quality of code output by the compiler.

3.7.4.1 Rationale

The following sections describe the rationale for restricted-pointer support.

3.7.4.1.1 Aliasing

For many compiler optimizations, ranging from simply holding a value in a register to the parallel execution of a loop, it is necessary to determine whether two distinct lvalues designate distinct objects. If the objects are not distinct, the lvalues are said to be aliases. If the compiler cannot determine whether or not two lvalues are aliases, it must assume that they are aliases and suppresses various optimizations.

Aliasing through pointers presents the greatest difficulty, because there is often not enough information available within a single function, or even within a single compilation unit, to determine whether two pointers can point to the same object. Even when enough information is available, this analysis can require substantial time and space. For example, it could require an analysis of a whole program to determine the possible values of a pointer that is a function parameter.

3.7.4.1.2 Library Examples

Consider how potential aliasing enters into implementations in C of two Standard C library functions memmove and memcpy :

  • There are no restrictions on the use of memmove , and the sample implementation that follows adheres to the model described in the revised ISO C Standard by copying through a temporary array.
  • Because memcpy cannot be used for copying between overlapping arrays, its implementation can be a direct copy.

The following example contrasts sample implementations of the memcpy and memmove functions:


/* Sample implementation of memmove */ 
 
   void *memmove(void *s1, const void *s2, size_t n) { 
           char * t1 = s1; 
           const char * t2 = s2; 
           char * t3 = malloc(n); 
           size_t i; 
           for(i=0; i<n; i++) t3[i] = t2[i]; 
           for(i=0; i<n; i++) t1[i] = t3[i]; 
           free(t3); 
           return s1; 
   } 
 
 
/* Sample implementation of memcpy */ 
 
   void *memcpy(void *s1, const void *s2, size_t n); 
           char * t1 = s1; 
           const char * t2 = s2; 
           while(n -> 0) *t1++ = *t2++; 
           return s1; 
   } 

The restriction on memcpy is expressed only in its description in the Standard, and cannot be expressed directly in its implementation in C. While this allows the source-level optimization of eliminating the temporary used in memmove , it does not provide for compiler optimization of the resulting single loop.

In many architectures, it is faster to copy bytes in blocks, rather than one at a time:

  • The implementation of memmove uses malloc to obtain the temporary array, and this guarantees that the temporary is disjoint from the source and target arrays. From this, a compiler can deduce that block copies can safely be used for both loops (if the compiler recognizes malloc as a special function that allocates new memory).
  • The implementation of memcpy , on the other hand, provides no basis for the compiler to rule out the possibility that, for example, s1 and s2 point to successive bytes. Therefore, unconditional use of block copies does not appear to be safe, and the code generated for the single loop in memcpy might not be as fast as the code for each loop in memmove .

3.7.4.1.3 Overlapping Objects

The restriction in the description of memcpy in the Standard prohibits copying between overlapping objects. An object is a region of data storage, and except for bit-fields, objects are composed of contiguous sequences of one or more bytes, the number, order, and encoding of which are either explicitly specified or implementation-defined.

Consider the following example:


/* memcpy between rows of a matrix */ 
 
void f1(void) { 
        extern char a[2][N]; 
        memcpy(a[1], a[0], N); 
} 

In this example:

  • The objects are exactly the regions of data storage pointed to by the pointers and dynamically determined to be of N bytes in length (that is, treated as an array of N elements of character type).
  • The objects are not the largest objects into which the arguments can be construed as pointing.
  • The call to memcpy has defined behavior.
  • The behavior is defined because the pointers point into different (non-overlapping) objects.

Now consider the following example:


/* memcpy between halves of an array */ 
 
void f2(void) { 
        extern char b[2*N]; 
        memcpy(b+N, b, N); 
} 

In this example:

  • Objects are defined as regions of data storage unrelated to declarations or types.
  • For memcpy , a contiguous sequence of elements within an array can be regarded as an object in its own right.
  • The objects are not the smallest contiguous sequence of bytes that can be construed; they are exactly the regions of data storage starting at the pointers and of N bytes in length.
  • The non-overlapping halves of array b can be regarded as objects in their own rights.
  • Behavior is defined.

The length of an object is determined by various methods:

  • For strings in which all elements are accessed, length is inferred by null-byte termination.
  • For mbstowcs , wcstombs , strftime , vsprintf , sscanf , sprintf , and all other similar functions, objects and lengths are dynamically determined.

3.7.4.1.4 Restricted Pointer Prototype for memcpy

If an aliasing restriction like the one for memcpy could be expressed in a function definition, then it would be available to a compiler to facilitate effective pointer alias analysis. The __restrict type qualifier accomplishes this by specifying in the declaration of a pointer that the pointer provides exclusive initial access to the object to which it points, as though the pointer were initialized with a call to malloc .

The following prototype for memcpy both expresses the desired restriction and is compatible with the current prototype:


void *memcpy(void * __restrict s1, const void * __restrict s2, size_t n); 

3.7.4.2 Formal Definition of the __restrict Type Qualifier

The following definition of restricted pointers supports expression of aliasing restrictions in as many paradigms as possible. This is helpful in converting existing programs to use restricted pointers, and allows more freedom of style in new programs.

This definition, therefore, allows restricted pointers to be:

  • Modifiable
  • Members of structures and elements of arrays
  • Strongly scoped, in the sense that a restricted pointer declared in a nested block makes a non-aliasing assertion only within that block

Definition

A pointer is designated as a restricted pointer by specifying the __restrict type qualifier on its declaration.

The formal definition of a restricted pointer as proposed for inclusion in the revised ISO C Standard follows:

Let D be a declaration of an ordinary identifier that provides a means of designating an object P as a restrict-qualified pointer.

If D appears inside a block and does not have storage-class extern, let B denote the block. If D appears in the list of parameter declarations of a function definition, let B denote the associated block. Otherwise, let B denote the block of main (or the block of whatever function is called at program startup, in a freestanding environment).

In what follows, a pointer expression E is said to be based on object P if (at some sequence point in the execution of B prior to the evaluation of E) modifying P to point to a copy of the array object into which it formerly pointed would change the value of E. (In other words, E depends on the value of P itself rather than on the value of an object referenced indirectly through P. For example, if identifier p has type (int ** restrict) , then the pointer expressions p and p+1 are based on the restricted pointer object designated by p , but the pointer expressions *p and p[1] are not.)

During each execution of B, let O be the array object that is determined dynamically by all references through pointer expressions based on P. All references to values of O shall be through pointer expressions based on P. Furthermore, if P is assigned the value of a pointer expression E that is based on another restricted pointer object P2, associated with block B2, then either the execution of B2 shall begin before the execution of B, or the execution of B2 shall end prior to the assignment. If this requirement is not met, then the behavior is undefined.

Here an execution of B means that portion of the execution of the program during which storage is guaranteed to be reserved for an instance of an object that is associated with B and has automatic storage duration. A reference to a value means either an access to or a modification of the value. During an execution of B, attention is confined to those references that are actually evaluated (this excludes references that appear in unevaluated expressions, and also excludes references that are "available," in the sense of employing visible identifiers, but do not actually appear in the text of B).

A translator is free to ignore any or all aliasing implications of uses of restrict.


Previous Next Contents Index