Type Qualifiers
In C (and C++) the variables and objects can be defined with type qualifiers which indicate special properties of the data. There are three type qualifiers:
- const
- volatile
- restrict
Const
The constant keyword const
tells the compiler that the data contained by a variable/object cannot change. Once a constant variable/object has been initialized, it cannot change its value.
Constant Variables
A variable of basic data type, structure, or rnum ca be declared as constant.e.g.
const int code = 203;
Once the constant variable has been initialized, its value cannot be changed. Any attempt to change the value will lead to compilation error.
#include <stdio.h>
int main()
{
const int code = 203;
code = 101;
return 0;
}
$ gcc const.c
const.c: In function 'main':
const.c:5: error: assignment of read-only variable 'code'
One thing to note is data that with variables of basic data type, enumeration, and structures, the const
keyword can be specified after specifying the data type of the variable as well i.e. both of the following declaration are synonymous
const int code = 203;
int const code = 203; // Same as above
Constant pointers
Pointers can be declared as constant as well. However, unlike normal variables the memory location to which the pointer points can be changed, therefore pointers can be declared as constant in three different ways:
- The pointer can point to a single location but the data at that location can be changed. This is done by specifying the
const
keyword before the pointer name in the definition i.e.#include <stdio.h>
int main()
{
char data1[] = "Hello World";
char data2[] = "BloodAxe";
char * const ptr = (char *)data1;
ptr[0] = 'A'; // Ok
ptr = (char *)data2; // Error
return 0;
}
$ gcc const.c -o const
const.c: In function 'main':
const.c:7: error: assignment of read-only variable 'ptr'
- The pointer can point to any location but the data at that location cannot be changed. This is done by specifying the
const
keyword before the datatype of the pointer in the definition #include <stdio.h>
int main()
{
char data1[] = "Hello World";
char data2[] = "BloodAxe";
const char * ptr = (char *)data1;
ptr = (char *)data2; // Ok
ptr[0] = 'A'; //Error
return 0;
}
$ gcc const.c -o const
const.c: In function 'main':
const.c:7: error: assignment of read-only location
- Neither the memory location to which the pointer points nor the data can be changed. This can be done by combining the two, i.e. specifying
const
keyword twice. Once before the datatype and then before the pointer name in the definition. #include <stdio.h>
int main()
{
char data1[] = "Hello World";
char data2[] = "BloodAxe";
const char * ptr = (char *)data1;
ptr = (char *)data2; // Error
ptr[0] = 'A'; //Error
return 0;
}
$ gcc const.c -o const
const.c: In function 'main':
const.c:6: error: assignment of read-only variable 'ptr'
const.c:7: error: assignment of read-only location
Constant Objects
In C++ the class objects can be declared as constant. If a class object has been declared as constant
- It cannot modify contents of the object members
- It cannot call any function that modifies the object members
#include <iostream>
class myClass
{
public:
int a;
myClass()
{ a = 20;}
void set(int value)
{
a = value;
}
};
int main()
{
const myClass m;
m.a= 10; // Not Ok
m.set(20); // Not Ok
return 0;
}
$ g++ const.cc
const.cc: In function 'int main()':
const.cc:18: error: assignment of data-member 'myClass::a' in read-only structure
const.cc:19: error: passing 'const myClass' as 'this' argument of 'void myClass::set(int)' discards qualifiers
Initialization Vs assignment
Constant variable/objects can be initialized but not assigned. This means that when the pointer is being defined, we can assign a value to it but not after it has been defined.
Initializing constant members in C++
In C++ if a constant is declared as part of the object, it needs to be assigned a value as part of the constructor's initialization list.
#include <iostream>
class myClass
{
private:
const int a;
public:
myClass():a(1)
{ }
};
int main()
{
myClass m;
return 0;
}
In the above code, the constant a
has been initialzed to value 1 in the constructor's initialization list. It is also possible to assign the constant to a value which has been paased to the constructor as an argument.
myClass(int value):a(value)
Volatile
The volatile type qualifier suggests that the value of the data/object can be modifed by an external program. This would mean that compiler should not try to optimize the variable/object.
A volatile variable is defined by specifying the voltile keyword volatile before the definition of the variable e.g. volatile int code;
There are certain scenarios where compiler might incorrectly try to optimize the memory access instructions. The volatile type qualifiers tries to tell the compiler to not to optimize those memory accesses. Such optimization can occur under following three scenarios:
- Memory mapped I/O
- setjmp and longjmp handling
- Accessing global variables in signal handler
Memory mapped I/O
In C a pointer can be pointed to an address which maps to an device i.e. a specific address might point to an device instead of a memory location and writing to an address will send the data to the device.
Consider the following code snippet
char far *ptr = (char far *) 0xB0008000L; // Point to video card buffer
ptr[0] = 'a';
ptr[0] = 'b';
ptr[0] = 'c';
In the above code the compiler might think that ptr[0] = 'a'
and ptr[0] = 'b'
are not necessary since the statement ptr[0] = 'c'
puts the value 'c' at that location. So the compiler might not generate assembly instructions for the first two statements where value 'a' and 'b' are being put to the location.
However, this may lead to incomplete data being sent to the device to which ptr is pointing. In order to avoid this, ptr should be declaed as volatile.
setjmp and longjmp handling
setjmp and longjmp is used to transfer the flow of execution from the called function to the calling functions without going through the stack unwinding processi.e. the flow of control is not passed through the intermediate called functions.
When the control is transferred back to the calling functions, the compiler may choose to not to restore the values of variables in the calling function.
Consider the following code snippet
#include <stdio.h>
#include <setjmp.h>
jmp_buf jmpbuffer;
void b()
{
longjmp(jmpbuffer,1);
}
void a()
{
b();
}
int main(void) {
int local_var ;
local_var = 10;
printf("Before jump: local_var:%d \n", local_var);
if (setjmp(jmpbuffer) != 0)
{
} else {
local_var = 20;
a();
}
printf("After jump: local_var:%d \n", local_var);
return 0;
}
The above program sets the variable local_var
to value 10 before setjmp is called and then modifies the value of local_var
to 20, before longjmp is called.
Following would be the output of the program
$ gcc jmp.c -o jmp
$ ./jmp
Before jump: local_var:10
After jump: local_var:20
Here, we can see that the local_var
was set to value 20 after the jump. However, let's see the output of the same program with optimization enabled.
$ gcc jmp.c -O3 -o jmp
$ ./jmp
Before jump: local_var:10
After jump: local_var:10
Here we see that with optimization enabled, the value of the variable local_var
gets restored to its previous value after the jump. Why did this happen ?
When setjmp is called, it saves the context of the program so that it can be restored when longjmp is called. Saving the context of the program involves saving the contents of the registers. And after the longjmp, the context including the register contents is restored.
Now, when the optimization is enabled, the compiler may choose to save the variable in register (for improving performance). This means that when the context is restored after longjmp, the contents of that variable will also be restored along with the contents of other registers.
In order to avoid this we can declare the variable as volatile, which will tell the compiler to not implement that variabe in a register but in a memory location.
#include <stdio.h>
#include <setjmp.h>
jmp_buf jmpbuffer;
void b()
{
longjmp(jmpbuffer,1);
}
void a()
{
b();
}
int main(void) {
volatile int local_var ;
local_var = 10;
printf("Before jump: local_var:%d \n", local_var);
if (setjmp(jmpbuffer) != 0)
{
} else {
local_var = 20;
a();
}
printf("After jump: local_var:%d \n", local_var);
return 0;
}
$ gcc jmp.c -O3 -o jmp
$ ./jmp
Before jump: local_var:10
After jump: local_var:20
Now, since the variable local_var
has been declared as volatile
, it retains the changes done after setjmp.
Accessing global variables in signal handler
If a signal handler is writing to a global variable/object, then its better to have it declared as volatile. This way the compiler will be aware the in other places that the value of hat variable/object might change unexpectedly.
Consider the following code snippet
define CONTINUE_DATA_TRANSFER 1
define CANCEL_DATA_TRANSFER 2
int transmit_data_status = CONTINUE_DATA_TRANSFER;
void my_signal_handler(int *data)
{
if (CANCEL_DATA_TRANSFER == (*data))
transmit_data_status = CANCEL_DATA_TRANSFER;
}
...
int main()
{
....
while (CONTINUE_DATA_TRANSFER == transmit_data_status)
{
send_data();
sleep(1);
}
....
}
In the above code, the signal handler my_signal_handler
set the variable transmit_data_status
to a desired value.
In the main function, the while loop continues to send data as long as transmit_data_status
is equal to CONTINUE_DATA_TRANSFER
. However, the compiler might optimize this statement considering the fact the variable transmit_data_status
is not modified anywhere in the loop so the check CONTINUE_DATA_TRANSFER == transmit_data_status)
is unecessary. The compiler might end up in removig the if check and would generate the assembly code corresponding to the following code
....
while (1)
{
send_data();
sleep(1);
}
....
Thus the while loop will become an infinite loop.
Now, to avoid this situation either the compiler can be told to disable the optimizations (which is not a good idea) or he compiler can be told that the variable transmit_data_status
can be modified by some external events or code. So, the compiler will not make such optimiations for the volatile variables.
Restrict
Restrict keyword is used with pointers. If a pointer is declared with the keyword restrict
then it means that the memory location or memory block to which the pointer points to will not be accessible by any other pointer. This can allow the copiler to make opimizations in the generated code. e.g. in the following code
int add (int * destination1, int * destination2,int * offset)
{
(*destination1) += (*offset);
(*destination2) += (*offset);
}
For the above function, the compiler might generate the following code
move A, *destination1 // Copy the contents of destination1 to register A
move B, *offset // Copy the contents of offset to register B
add C,A,B // Add register A and B and put the result in register C
move *destination1, C // Copy the contents of register C to destination1
move A, *destination2 // Copy the contents of destination2 to register A
move B, *offset // Copy the contents of offset to register B
add C,A,B // Add register A and B and put the result in register C
move *destination2, C // Copy the contents of register C to destination2
In the above code the stamenet move B, *offset
is generated twic, because the compiler might not be sure if the move operation move *destination1, C would change the contents of offset
or not.
However, with restrict
keywork we can tell the compiler that pointer offset
would not overlap in the memory with any other pointer i.e. changing any other pointer will not cause the contents of offset
pointer to change.
int add (int * destination1, int * destination2,int * restrict offset)
{
(*destination1) += (*offset);
(*destination2) += (*offset);
}
Based on this knowledge the compiler can generate optimized code and would not need to load the contents of offset
pointer again.
move A, *destination1 // Copy the contents of destination1 to register A
move B, *offset // Copy the contents of offset to register B
add C,A,B // Add register A and B and put the result in register C
move *destination1, C // Copy the contents of register C to destination1
move A, *destination2 // Copy the contents of destination2 to register A
add C,A,B // Add register A and B and put the result in register C
move *destination2, C // Copy the contents of register C to destination2
However, if a pointer is declared as restrict
then it is the compiler's responsibility to ensure that the pointer does not overlap with any other pointer. An incorrect usage of restrict
can lead to abnoraml behaviour.
Note: Support for restrict
was added later on in the C99 standard, so older versions of compilers might not support it. For some compilers a switch needs to be specified e.g. -std=c99
for the compiler to recognize this keyword.
gcc -std=c99 restrict.c