What Are C Type Qualifiers and their uses in programming

In this blog post, you will learn the C Type Qualifiers and their concept. You will learn how to use C-type Qualifiers with identifiers and their effect on them. We also see some programming examples to understand the qualifiers.

 

What are Type Qualifiers in C?

In the C programming languages, a type qualifier is a keyword that is applied to a type, resulting in a qualified type. Let’s understand it with an example, const int is a qualified type representing a constant integer, where const is a type qualifier.

C supports 4 types of qualifiers these are const, restrict, volatile , and _Atomic. The const keyword is compiler-enforced and says that the program could not change the value of the object that means it makes the object a nonmodifiable type. An object that has a volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.

I have already written few articles on the const and volatile keyword if you want you can check.

 

Type Qualifiers support by C?

There are four type qualifiers in standard C: const (C89), volatile (C89), restrict (C99), and _Atomic (C11). Let’s see these qualifiers one by one.

const qualifier:

The const type qualifier declares an object to be nonmodifiable. The const keyword specifies that a variable’s value is constant and tells the compiler to prevent the programmer from modifying it. So we use a const qualifier when we don’t want to change the value of any object in our program. Let’s consider the below example code.

#include <stdio.h>

int main()
{
    const int i = 5;

    i = 10;   //error

    i++;    //error
    return 0;
}

In the above code, we have used the const keyword with the variable “i“. When we will try to modify it we will get the compiler error because we can not assign value to const int.

Note: In C++, you can use the const keyword instead of the #define preprocessor directive to define constant values.

 

If an attempt is made to modify an object defined with a const-qualified type through the use of an lvalue with a non-const-qualified type, the behavior is undefined. Let’s see a code.

#include <stdio.h>

int main()
{
    //const-qualified integer
    const int data = 5;
    
    //non-const-qualified type
    int * ptr = NULL;

    ptr = (int*)&data;
    
    *ptr = 6;
    
    printf("*ptr = %d",*ptr);
    
    return 0;
}

Output: Behavior is undefined (UB).

 

Like the simple variable, we can also use the const keyword with pointers. The const keyword is useful for declaring pointers to const since this requires the function not to change the pointer in any way. Let’s see some legal const and pointer declarations:

//The following are legal const declarations with pointer:


int const *ptr;      // Pointer to constant int

const int *ptr;   // Pointer to constant int

int *const ptr;     // Constant pointer to int

int (*const ptr);   // Constant pointer to int

const int *const ptr;     // Constant pointer to const int

int const *const ptr;     // Constant pointer to const int

 

In C, constant values default to the external linkage, so they can appear only in source files. So when you declare a variable as const in a C source code file like below.

const int data = 10;

You can then use this variable in another module as follows:

extern const int data;

 

Note: The implementation can place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.

 

volatile qualifier:

The volatile keyword is a type qualifier that prevents the objects from compiler optimization. The compiler assumes that, at any point in the program, a volatile variable can be accessed by unknown factors that use or modifies its value.

According to the C standard, an object that has a volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.

Like the const, we can use the volatile qualifier with variable. See the below declaration where I am using the volatile qualifier with the integer variable.

//Both are same

int volatile data1;

volatile int data2;

 

 

If you try to modify an object defined with a volatile-qualified type through the use of an lvalue with a non-volatile-qualified type, the behavior is undefined. Let’s see an example code.

#include <stdio.h>

int main()
{
    volatile int data = 5;
    int *ptr = (int*)(&data);

    *ptr =4;

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

    return 0;
}

Output: Behavior is undefined (UB).

 

Like the const keyword, we can also use the volatile keyword with pointers. To declare the object pointed to by the pointer as volatile, use a declaration of the form:

//The following are legal volatile declarations with pointer:

int volatile *ptr;      // Pointer to volatile int

volatile int *ptr;   // Pointer to volatile int

int *volatile ptr;     // volatile pointer to int

int (*volatile ptr);   // volatile pointer to int

volatile int *volatile ptr;     // volatile pointer to volatile int

int volatile *volatile ptr;     // volatile pointer to volatile int

 

In C we can use both volatile and const together. We can qualify a variable with both const and volatile keywords. In this case, the variable couldn’t be legitimately modified by its own program but could be modified by some asynchronous process.

volatile const int data = 50;

 

restrict qualifier:

The restrict type qualifier, introduced in C99 and it is a special type qualifier and can be applied to pointer declarations. It qualifies the pointer, not what it points at. An object that is accessed through a restrict-qualified pointer has a special association with that pointer.

Basically, restrict is an optimization hint to the compiler that no other pointer in the current scope refers to the same memory location. That is, only the pointer (ptr) or a value derived from it (such as ptr + 1) is used to access the object during the lifetime of the pointer. This helps the compiler produce more optimized code.

Let’s see an example to understand how to restrict keywords optimize the code. Let consider the below function. The configuration which I am using, compiler x86-64 gcc (trunk)  with settings -std=c17 -O3.

Case 1: function with restrict keyword

void copyArray(int n, int * restrict p, int * restrict q)
{
    while (n-- > 0)
    {
        *p++ = *q++;
    }
}

 

The compiler generates below assembly code.

copyArray:
        movslq  %edi, %rax
        movq    %rsi, %rdi
        movq    %rdx, %rsi
        testl   %eax, %eax
        jle     .L1
        leaq    0(,%rax,4), %rdx
        jmp     memcpy
.L1:
        ret

 

Case 2: function without restrict keyword

Now remove the restrict keyword from the function and check the assembly code generated by the compiler with the same configuration.

void copyArray(int n, int *p, int *q)
{
    while (n-- > 0)
    {
        *p++ = *q++;
    }
}

 

The compiler generates below assembly code without the restrict keyword. You can see that code is less optimized.

copyArray:
        movl    %edi, %r8d
        movq    %rsi, %rcx
        leal    -1(%rdi), %edi
        testl   %r8d, %r8d
        jle     .L1
        leaq    4(%rdx), %rsi
        movq    %rcx, %rax
        subq    %rsi, %rax
        cmpq    $8, %rax
        jbe     .L3
        cmpl    $2, %edi
        jbe     .L3
        movl    %r8d, %esi
        xorl    %eax, %eax
        shrl    $2, %esi
        salq    $4, %rsi
.L4:
        movdqu  (%rdx,%rax), %xmm0
        movups  %xmm0, (%rcx,%rax)
        addq    $16, %rax
        cmpq    %rsi, %rax
        jne     .L4
        movl    %r8d, %esi
        andl    $-4, %esi
        movl    %esi, %eax
        subl    %esi, %edi
        salq    $2, %rax
        addq    %rax, %rcx
        addq    %rdx, %rax
        andl    $3, %r8d
        je      .L1
        movl    (%rax), %edx
        movl    %edx, (%rcx)
        testl   %edi, %edi
        jle     .L1
        movl    4(%rax), %edx
        movl    %edx, 4(%rcx)
        cmpl    $1, %edi
        jle     .L1
        movl    8(%rax), %eax
        movl    %eax, 8(%rcx)
        ret
.L3:
        movslq  %r8d, %rsi
        xorl    %eax, %eax
.L6:
        movl    (%rdx,%rax,4), %edi
        movl    %edi, (%rcx,%rax,4)
        addq    $1, %rax
        cmpq    %rsi, %rax
        jne     .L6
.L1:
        ret

 

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

 

_Atomic qualifier:

The _Atomic qualifier shall not be used if the implementation does not support atomic types. The properties associated with atomic types are meaningful only for expressions that value. If the _Atomic keyword is immediately followed by a left parenthesis, it is interpreted as a type specifier (with a type name), not as a type qualifier.

For example:

_Atomic ( type-name )	(1)	(since C11)


_Atomic type-name	(2)	(since C11)

1) _Atomic use as a type specifier.

2) _Atomic use as a type qualifier.

Note: The type modified by the _Atomic qualifier shall not be an array type or a function type.

 

If you love online courses and want to learn C programming, you can check the below courses it will help.

 

Recommended Post