📜 ⬆️ ⬇️

So you think you know Const?

From the translator:
I suggest you translate the post from Matt Stancliff 's blog, the author of the article on Habré's article Tips on how to write in C in 2016 .
Here Matt shares knowledge about the const type qualifier. Despite the challenging title, perhaps much of what is described here will be known to you, but I hope there will be something new too.
Enjoy reading.

Think you know all the rules for using const for C? Think again.

Basics const


Scalar variables


You are familiar with the simple const rule in C.
 const uint32_t hello = 3; 

const before hello means that during compilation there is a check that hello never changes.

If you try to change or override hello , the compiler will stop you:
 clang-700.1.81: error: read-only variable is not assignable hello++; ~~~~~^ error: read-only variable is not assignable hello = 92; ~~~~~ ^ gcc-5.3.0: error: increment of read-only variable 'hello' hello++; ^ error: assignment of read-only variable 'hello' hello = 92; ^ 

In addition, C doesn’t worry much about where the const is located as long as it is in front of the identifier, so the const uint32_t and uint32_t const declarations are identical:
 const uint32_t hello = 3; uint32_t const hello = 3; 

Scalar variables in prototypes


Compare the prototype and implementation of the following function:
 void printTwo(uint32_t a, uint64_t b); void printTwo(const uint32_t a, const uint64_t b) { printf("%" PRIu32 " %" PRIu64 "\n", a, b); } 

Will the compiler swear if the scalar parameters with the const qualifier are specified in the printTwo() function implementation and without it in the prototype?
')
Nope

For scalar arguments, it is perfectly normal that the const qualifiers do not match in the prototype and function implementation.
Why is it good? It's very simple: your function cannot change a and b outside its scope, so const has no effect what you pass on to it. Your compiler is smart enough to understand that these are copies of a and b , so in this case the presence or absence of const has no effect on the physical or mental models of your program.

Your compiler does not care about the mismatch of the const qualifier for any parameters that are not pointers or arrays, since they are copied into the function by value and the initial value of the passed variables always remains constant 1 .

However, your compiler will complain about the inconsistency of const for parameters that are pointers or arrays, because in this case your function will be able to manipulate the data that the passed pointer refers to.

Arrays


You can specify const for the entire array.
 const uint16_t things[] = {5, 6, 7, 8, 9}; 

const can also be specified after the type declaration:
 uint16_t const things[] = {5, 6, 7, 8, 9}; 

If you try to change things[] , the compiler will stop you:
 clang-700.1.81: error: read-only variable is not assignable things[3] = 12; ~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location 'things[3]' things[3] = 12; ^ 


Structures


Ordinary structures


You can specify const for the whole structure.
 struct aStruct { int32_t a; uint64_t b; }; const struct aStruct someStructA = {.a = 3, .b = 4}; 

Or:
 struct const aStruct someStructA = {.a = 3, .b = 4}; 

If we try to change any member of someStructA :
 someStructA.a = 9; 

We get an error because someStructA declared as const . We cannot change its members after definition.
 clang-700.1.81: error: read-only variable is not assignable someStructA.a = 9; ~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of member 'a' in read-only object someStructA.a = 9; ^ 

const inside structure


You can specify const for individual members of the structure:
 struct anotherStruct { int32_t a; const uint64_t b; }; struct anotherStruct someOtherStructB = {.a = 3, .b = 4}; 

If we try to change someOtherStructB members:
 someOtherStructB.a = 9; someOtherStructB.b = 12; 

We get an error only when b changed, because b declared as const :
 clang-700.1.81: error: read-only variable is not assignable someOtherStructB.b = 12; ~~~~~~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only member 'b' someOtherStructB.b = 12; 

Declaring an entire instance of a structure with the const qualifier is equivalent to declaring a special copy of the structure in which all members are defined as const . If you do not need a 100% const structure, you can specify const only for specific members when a structure is declared, only where it is needed.

Pointers


const for pointers is where the fun begins.

One const


Let's use the integer pointer as an example.
 uint64_t bob = 42; uint64_t const *aFour = &bob; 

Since this is a pointer, there are two repositories here:

So what can we do with aFour ? Let's try a few things.
Do you think that the value he points to can be changed?
 *aFour = 44; 

 clang-700.1.81: error: read-only variable is not assignable *aFour = 44; ~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '*aFour' *aFour = 44; ^ 

How about updating the const pointer without changing the value it points to?
 aFour = NULL; 

It really works and is perfectly acceptable. We have declared uint64_t const * , which means "pointer to immutable data", but the pointer itself is not immutable (note also: const uint64_t * also has a meaning).

How to make both data and pointer unchanged at the same time? Meet the double const .

Two const


Let's add one more const and see how things go.
 uint64_t bob = 42; uint64_t const *const anotherFour = &bob; *anotherFour = 45; anotherFour = NULL; 

What is the result?
 clang-700.1.81: error: read-only variable is not assignable *anotherFour = 45; ~~~~~~~~~~~~ ^ error: read-only variable is not assignable anotherFour = NULL; ~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '*anotherFour' *anotherFour = 45; ^ error: assignment of read-only variable 'anotherFour' anotherFour = NULL; ^ 

Yeah, we managed to make both the data and the pointer itself immutable.

What does const *const mean?
The meaning here seems less obvious.
The value is so shaky, because in fact it is recommended to read variable declarations from right to left (or even worse, spiral ).
In this case, if you read from right to left 2 This announcement means:
 uint64_t const *const anotherFour = &bob; 

anotherFour is:

Take our “regular” syntax and read from right to left:
 uint64_t const *aFour = &bob; 

aFour is:

What did we just see?
There is an important distinction: people usually call const uint64_t *bob as an “immutable pointer”, but this is not what is happening here. This is actually a “mutable pointer to immutable data”.

Interlude - explaining const declarations


But wait, more - more!

We just saw how the pointer view gave us four different options for declaring the const qualifier. We can:

This is what concerns one pointer and two const , but what if we add another pointer?

Three const


One


How many ways can we use to add const to a double pointer?

Let's quickly check it out.
 uint64_t const **moreFour = &aFour; 

Which of these operations are allowed, based on the ad above?
 **moreFour = 46; *moreFour = NULL; moreFour = NULL; 

 clang-700.1.81: error: read-only variable is not assignable **moreFour = 46; ~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**moreFour' **moreFour = 46; ^ 

Only the first assignment did not work, because if we read our declaration from right to left:
 uint64_t const **moreFour = &aFour; 

moreFour is:

As we can see, the only operation that we could not perform is the change in the stored value. We have successfully changed the pointer and the pointer to the pointer.

Two


What if we want to add another const modifier to a level deeper?
 uint64_t const *const *evenMoreFour = &aFour; 

Given two const 3 What can we do now?
 **evenMoreFour = 46; *evenMoreFour = NULL; evenMoreFour = NULL; 

 clang-700.1.81: error: read-only variable is not assignable **evenMoreFour = 46; ~~~~~~~~~~~~~~ ^ error: read-only variable is not assignable *evenMoreFour = NULL; ~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**evenMoreFour' **evenMoreFour = 46; ^ error: assignment of read-only location '*evenMoreFour' *evenMoreFour = NULL; ^ 

Now we are twice protected from changes, because if we read our ad from right to left:
 uint64_t const *const *evenMoreFour = &aFour; 

evenMoreFour is:


Three


We can do a little better than two. Meet three const .

What if we want to block all changes when declaring a double pointer?
 uint64_t const *const *const ultimateFour = &aFour; 

What can we (not) do now?
 **ultimateFour = 48; *ultimateFour = NULL; ultimateFour = NULL; 

 clang-700.1.81: error: read-only variable is not assignable **ultimateFour = 46; ~~~~~~~~~~~~~~ ^ error: read-only variable is not assignable *ultimateFour = NULL; ~~~~~~~~~~~~~ ^ error: read-only variable is not assignable ultimateFour = NULL; ~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**ultimateFour' **ultimateFour = 46; ^ error: assignment of read-only location '*ultimateFour' *ultimateFour = NULL; ^ error: assignment of read-only variable 'ultimateFour' ultimateFour = NULL; ^ 

Nothing works! Success!

Let's go again:
 uint64_t const *const *const ultimateFour = &aFour; 

ultimateFour is:

Additional rules




Khaki


Khaki type cast


What if you are smart and have created a mutable pointer to an immutable repository?
 const uint32_t hello = 3; uint32_t *getAroundHello = &hello; *getAroundHello = 92; 

Your compiler will complain that you are dropping const , but just giving a warning 4 which you can disable 5 .
 clang-700.1.81: warning: initializing 'uint32_t *' (aka 'unsigned int *') with an expression of type 'const uint32_t *' (aka 'const unsigned int *') discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers] uint32_t *getAroundHello = &hello; ^ ~~~~~~ gcc-5.3.0: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] uint32_t *getAroundHello = &hello; ^ 

Since this is C, you can drop the const qualifier by explicit type conversion and get rid of the warning (as well as the const initialization violation):
 uint32_t *getAroundHello = (uint32_t *)&hello; 

Now you have no warnings when compiling because you explicitly instructed the compiler to ignore the real &hello type and use uint32_t * instead.

Khaki memory


What if the structure contains const members, but will you change the data stored in it after the declaration?

Let's declare two structures that differ only in the constancy of their members.
 struct exampleA { int64_t a; uint64_t b; }; struct exampleB { int64_t a; const uint64_t b; }; const struct exampleA someStructA = {.a = 3, .b = 4}; struct exampleB someOtherStructB = {.a = 3, .b = 4}; 

Let's try copying someOtherStructB into const someStructA .
 memcpy(&someStructA, &someOtherStructB, sizeof(someStructA)); 

Will this work?
 clang-700.1.81: warning: passing 'const struct aStruct *' to parameter of type 'void *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers] memcpy(&someStructA, &someOtherStructB, sizeof(someStructA)); ^~~~~~~~~~~~ gcc-5.3.0: In file included from /usr/include/string.h:186:0: warning: passing argument 1 of '__builtin___memcpy_chk' discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] memcpy(&someStructA, &someOtherStructB, sizeof(someStructA)); ^ note: expected 'void *' but argument is of type 'const struct aStruct *' 

Nope, it doesn't work, because prototype 6 for memcpy it looks like this:
 void *memcpy(void *restrict dst, const void *restrict src, size_t n); 

memcpy does not allow passing unchanged pointers to it as a dst argument, since dst changes during copying (and someStructA immutable).

Although, const parameters are checked only by the function prototype. Will the compiler complain if we use a partially immutable structure with separate const fields as dst ?

What happens if we try to copy const someStructA into a variable, but containing one const member someOtherStructB ?
 memcpy(&someOtherStructB, &someStructA, sizeof(someOtherStructB)); 

Now the function prototype check passes and we do not receive warnings about memcpy , even despite the fact that we have overwritten the immutable member of the not completely immutable structure.

Conclusion


Do not create mutable values ​​unless necessary. Be careful that your program actually works as you planned.

Try it yourself
 #include <stddef.h> /*   NULL */ #include <stdint.h> /*      */ int main(void) { uint64_t bob = 42; const uint64_t *aFour = &bob; /* uint64_t const *aFour = &bob; */ *aFour = 44; /*  */ aFour = NULL; const uint64_t *const anotherFour = &bob; /* uint64_t const *const anotherFour = &bob; */ *anotherFour = 45; /*  */ anotherFour = NULL; /*  */ const uint64_t **moreFour = &aFour; /* uint64_t const **moreFour = &aFour; */ **moreFour = 46; /*  */ *moreFour = NULL; moreFour = NULL; const uint64_t *const *evenMoreFour = &aFour; /* uint64_t const *const *evenMoreFour = &aFour; */ **evenMoreFour = 47; /*  */ *evenMoreFour = NULL; /*  */ evenMoreFour = NULL; const uint64_t *const *const ultimateFour = &aFour; /* uint64_t const *const *const ultimateFour = &aFour; */ **ultimateFour = 48; /*  */ *ultimateFour = NULL; /*  */ ultimateFour = NULL; /*  */ return 0; } 






1 - this also means that it is absolutely safe to transfer const scalars to a function that uses them as non- const parameters, since it cannot change the original values ​​of scalar variables in any way. ^

2 - in such cases, it may be better to write uint64_t const * instead of const uint64_t * , since both of these declarations lead to exactly the same result, but reading your ad from right to left becomes more convenient if the const qualifier follows the type. ^

3 - it also unconditionally confirms that the correct syntax for pointers is type *name , not type* name and certainly not type * name because when we add const , the pointer is attached to the next qualifier, not the previous one. For example:
Wrong
 uint64_t const* const* evenMoreFour; /*       const */ 

Right
 uint64_t const *const *evenMoreFour; /* const    . */ 
^

4 - well, you need to use a non-standardized flag depending on the compiler model, so the build process may require a lot of redundant flags for compatibility with different compilers to disable these warnings. ^

5 - I remind: const checked only at compile time; it does not change the program’s behavior only if you don’t manage to break the restrictions imposed by const (no more than changing any other value would change the behavior of your program), but it probably will not work as you expect. Also: your compiler can place immutable data in read-only code segments, and trying to bypass these const blocks can lead to undefined behavior. ^

6 - also note the restrict keyword in the memcpy() prototype. restrict means “this pointer data does not overlap with other data in the current scope”, which determines how memcpy() plans to process its parameters.
If when copying a pointer to the destination, partially overlaps the pointer to the place where the data memmove() from, you need to use the memmove() function, its prototype does not contain restrict qualifiers.
 void *memmove(void *dst, const void *src, size_t len); 
^

Source: https://habr.com/ru/post/301332/


All Articles