Array declared without size and const volatile - c

I found this line in my work project, but I can't understand this:
extern const volatile uint8 * const volatile array[];
Could you please help me explain the line?

First, leaving out the qualifiers:
uint8 *array[];
array is an array of unspecified size whose elements are of type uint8 *. In other words, an array of pointers to uint8.
If this declaration appears as a parameter to a function, the array syntax is actually shorthand for a pointer. If it is not a function parameter and is declared at file scope then this acts as a tentative definition. A complete definition may occur elsewhere in the code that specifies the size and optionally an initializer. If no other complete definition exists, then the array is defined to have 1 element.
Before talking about what the declaration means with the qualifiers, let's first talk about exactly what those qualifiers mean.
The const qualifier prevents code from modifying the named object. Given this declaration:
const int x;
It means that x can't be modified using for example x = 1. With pointers involved it's a little more tricky:
const int *x;
This defines x as a pointer to a const int. That means that you can modify x but not what it points to. So x = &y is legal but not *x = 1.
int * const x;
This defines x as a const pointer to an int, which means you can't modify x but you can modify what it points to. So x = &y is not legal but *x = 1 is.
const int * const x;
This defines x as a const pointer to a const int. So in this case, neither x nor what it points to can be modified.
const int * const x[];
Here, x is an array whose elements are const pointers to a const int. As in the prior example, for each array element, neither the array element nor what it points to can be modified.
Now let's talk about volatile. This qualifier tells the compiler that the variable in question might change unpredictably. From section 6.7.3p7 of the C standard:
An object that has volatile-qualified type may be modified in
ways unknown to the implementation or have other unknown side
effects. Therefore any expression referring to such an object shall
be evaluated strictly according to the rules of the abstract machine,
as described in 5.1.2.3. Furthermore, at every sequence point the
value last stored in the object shall agree with that prescribed by
the abstract machine, except as modified by the unknown factors
mentioned previously. 134) What constitutes an access to an
object that has volatile-qualified type is implementation-defined
134) A volatile declaration may be used to describe an object
corresponding to a memory-mapped input/output port or an object
accessed by an asynchronously interrupting function. Actions
on objects so declared shall not be "optimized out" by an
implementation or reordered except as permitted by the rules for
evaluating expressions
What this means is that a volatile object could change in ways not known to the compiler, and thus the compiler should not perform any optimizations on this variable and in fact should assume the value was changed externally.
Now moving on the your full declaration:
const volatile uint8 * const volatile array[];
This declares array as an array of unspecified size whose elements are of type uint8 *, where the elements of the array cannot be modified by the program (i.e. are const) but could be modified externally (i.e. volatile), and what those array elements point to also cannot be changed by the program but could be modified externally.

Related

const / static / volatile in array type specification?

What does the following function signature define in C?!
#include <stdio.h>
#include <string.h>
void f(int a[const volatile static 2])
{
(void)a;
}
int main() {
int b[1];
f(b);
}
https://godbolt.org/z/6qPxaM1vM
I don't get the meaning of const / volatile / static at this place, but it seems to compile, so I guess it has a meaning?
Thanks
This is a mildly useful feature introduced in C99. From C17 6.7.6.3/7:
A declaration of a parameter as “array of type” shall be adjusted to “qualified pointer to type”, where
the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the
keyword static also appears within the [ and ] of the array type derivation, then for each call to
the function, the value of the corresponding actual argument shall provide access to the first element
of an array with at least as many elements as specified by the size expression.
Meaning in this case the qualifiers const and volatile means that the array decays into a pointer of type int* const volatile a. Since this qualified type is local to the function, it means very little to the caller, since a passed pointer is assigned by lvalue conversion and can still be a non-qualified pointer type.
The static is ever so slightly more useful, as it supposedly enables some compile-time size checks by the compiler, though in practice it seems that the mainstream compilers (currently) only manage to check that the pointer is not null. For example f(0) on clang gives:
warning: null passed to a callee that requires a non-null argument [-Wnonnull]
Curiously, f(0) on gcc 11.1 and beyond says:
warning: argument 1 to 'int[static 8]' is null where non-null expected [-Wnonnull]"
No idea what the 8 is coming from, I guess it's a typo/minor compiler bug (the decayed pointer is 8 bytes large).
A parameter of array type is automatically adjusted to a pointer type.
Therefore a parameter declared as:
int A[3]
is transformed to:
int *A
However, with the array notation there is no intuitive place to add qualifier the variable a itself (not the data pointed by a).
Therefore C standard allows to put those specifier between brackets.
Thus:
void f(int a[const volatile restrict 2])
is actually:
void (int * const volatile restrict a)
The size (2 in above example) is usually ignored. The exception is when static keyword is used. It provides a hint for a compiler that at least elements at addresses a + 0 to a + size - 1 are valid.
This hint in theory should improve optimization by simplifying vectorization. However, AFAIK it is ignored by major compilers.
Your code:
int b[1];
f(b);
is triggering UB because only element at address b + 0 is valid while the hint requires b+0 and b+1 to be valid. Better compilers/sanitizers should detect that.
The static is also useful for self documentation and detection of errors like telling that at least n elements pointed by a pointer must be valid:
void fun(int n, int arr[static n])
or even telling that the pointer is never NULL:
void fun(int ptr[static 1]);
Moreover syntax int buf[static n] is a good visual hint that something is actually not an array. It help to avoid a common error when trying to acquire the a size of "array" with sizeof buf syntax.
EDIT
As stated on the comment the word "hint" may be a bit misleading
because it could be interpreted that violation the "hint" is not
an error though it may result in some non-optimality (like performance degradation).
Actually, it is rather a requirement, violating which results in undefined behavior.

const declaration with array arguments?

So I was reading the book Language C by Kernighan Ritchie and on page 39, Chapter 2: Types, Operators and Expressions
the author writes:
The const declaration can also be used with array arguments, to indicate that the function does not change that array:
int strlen(const char[]);
The result is implementation-defined if an attempt is made to change a const.
I don't understand what it means. Would appreciate if anyone could simplify what he means by that.
"Implementation defined" simply means that it is up to the implementation what should happen. A difference from "undefined behavior" is that when it is "implementation defined", the behavior needs to be documented. Read more about that here: Undefined, unspecified and implementation-defined behavior
But you can change things via a const pointer if you cast it to non-const. This will print 42;
void foo(const int *x)
{
*(int *)x = 42;
}
int main(void)
{
int n = 69;
foo(&n);
printf("%d\n", &n);
}
I wrote a related answer about const that you can read here: https://stackoverflow.com/a/62563330/6699433
Declaring function parameters as const indicates that the function should not change the value of those parameters.
Even though C passes arguments by value, pointed-to values are susceptible to change. By declaring the function parameter as const, if the function attempts to modify the pointed-to value, the compiler will generate an error.
The following function will change the value pointed to by x:
void foo(int *x)
{
*x = 100;
}
In the following function, by marking the parameter as const, the function will not be able to change the value pointed to by x.
void foo(const int *x)
{
*x = 100; // Compiler generates an error
}
In C, even though it looks like you're passing an array when using the square brackets [], you're actually passing a pointer. So void foo(const int *x) would be the same as void foo(const int x[])
Summary
Kernighan and Ritchie are wrong; attempting to modify const objects is undefined, not implementation-defined.
This rules applies only to objects originally defined with const.
const on function parameters is advisory, not enforced. It is possible, and defined by the C standard, for a function to modify an object pointed to with a const parameter if that object was not defined with const.
Details
The quoted passage is wrong. Attempting to modify an object defined with const has undefined behavior, not implementation-defined behavior. And this applies only to objects defined with const, not to objects passed via const-qualified pointers, if those objects were not originally defined with const.
C 2018 6.7.3 7 says:
If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.
The same wording appears in C 1990 6.5.3.
“Undefined“ means the C standard does not impose any requirements on the behavior (C 2018 3.4.3). This is different from “implementation-defined,” which means the C implementation must document how a choice among possibilities is made (C 2018 3.4.1).
Note that that rule applies only to objects defined with const. 6.7 5 tells us that, for object identifiers, a definition is a declaration that causes storage to be reserved for the object. If we declare int x; inside a function, that will cause storage to be reserved for x, so it is a definition. However, the statement int strlen(const char[]); merely declares a function and its parameter type. The actual parameter is not declared because there is no name for it. If we consider the actual function definition, such as:
int strlen(const char s[])
{
…
}
then this function definition includes a declaration of the parameter s. And it does define s; storage for the parameter itself will be reserved when the function executes. However, this s is only a pointer to some object that the caller passes the address of. So this is not a definition of that object.
So far, we know the rule in 6.7.3 7 tells us that modifying an object defined with const has undefined behavior. Are there any other rules about a function modifying an object it has received through a pointer with const? There are. The left operand of an assignment operator must be modifiable. C 2018 6.5.16 2 says:
An assignment operator shall have a modifiable lvalue as its left operand.
An lvalue qualified with const is not modifiable, per C 2018 6.3.2.1 1. This paragraph is a constraint in the C standard, which means a C implementation is required to diagnose violations. (So, again, this is not implementation-defined behavior. The C implementation must produce a message.) The ++ and -- operators, both pre- and post-, have similar constraints.
So, a function with a parameter const char s[] cannot directly modify *s or s[i], at least not without getting a diagnostic message. However, a program is allowed to remove const in a conversion operator if it was not originally present. C 2018 6.3.2.3 2 says we can add const:
For any qualifier q, a pointer to a non-q-qualified type may be converted to a pointer to the q-qualified version of the type; the values stored in the original and converted pointers shall compare equal.
and then C 2018 6.3.2.3 7 says that, after we have done that, we can convert the const version back to the original type:
A pointer to an object type may be converted to a pointer to a different object type… when converted back again, the result shall compare equal to the original pointer.
What this means is that if a calling routine has:
int x = 3;
foo(&x);
printf("%d\n", x);
and foo is:
void foo(const int *p)
{
* (int *) p = 4;
}
then this is allowed and defined by the C standard. The function foo removes const and modifies the object it points to, and “4” will be printed.
A lesson here is that const in function parameters is advisory, not enforced by C. It serves two purposes:
const on a function parameter is generally an indication for humans that the function will not modify the pointed-to object through that parameter. (However, there are circumstances, not discussed here, where this indication does not hold.)
The compiler will enforce a rule that the pointed-to object cannot be modified through the const type. This prevents inadvertent errors where a typographical error might result in an unwanted assignment to a const object. However, a function is permitted to explicitly remove const and then attempt to modify the object.
That material appears a bit obsolete.
The strlen standard library function returns size_t nowadays, but anyway:
int strlen(const char[]);, which is the same as, int strlen(const char*); means
strlen can accept either a char* or const char* without needing a cast.
If you pass a pointer to a non-const variable and the function attempts to modify it (by casting away the const as in void modify(char const *X){ *(char*)X='x'; }) the behavior is ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶a̶t̶i̶o̶n̶-̶d̶e̶f̶i̶n̶e̶d̶ undefined (it is undefined, not implementation defined in newer C versions).
Undefined behavior means you lose all guarantees about the program.
Implementation defined means the code will do something specific (e.g., abort, segfault, do nothing) in a consistent, predictable fashion (given a platform) and that it that won't affect the integrity of the rest of the program.
In older simple-minded compilers, a static-lifetime const-variable would be placed in read-only memory if the platform has read-only memory or in writable memory otherwise. Then you either get a segfault on an attempt to modify it or you won't. That would be implementation-defined behavior.
Newer compilers may additionally attempt to do far-reaching optimization based on the const annotations, so it's hard to tell what effects an attempt at such a modification attempt may have. The fact that such a modification attempt is undefined behavior in modern C allows compilers to make such optimizations (i.e., optimizations which yield hardly predictable results if the rule is broken).
const char[] as parameter type of a function is in fact equal to a pointer to const char (type const char *). The array notation was invented for convenience when passing pointer to arrays.
Related posts:
C pointer notation compared to array notation: When passing to function
Passing an array as an argument to a function in C
With declaring const char p_a[] you declare p_a as a pointer to const char.
The const qualifier associated to char tells the compiler that the char object/array pointed to in the caller shouldn't be modified.
Any attempt to modify this object/array with a non-const pointer/lvalue invokes undefined behavior:
"If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined."
Source: C18, 6.7.3/7
and a compiler usually will warn you when doing so.
strlen does not need to and also should not modify a string to which a pointer is passed as argument. For the sake of not giving any chance to accidentally modifying the string in the caller, the pointer is classified as pointer to const char.
The const qualifier adds an extra layer of security.

Could this memcpy result in undefined behavior?

With this definition:
struct vector {
const float x;
const float y;
};
would the code snippet below possibly result in undefined behavior?
struct vector src = {.x=1.0, .y=1.0};
struct vector dst;
void *dstPtr = &dst;
memcpy(dstPtr, &src, sizeof dst);
gcc and clang do not emit any warnings, but it does result in modification of a const-qualified type.
The construct looks a lot like the one given in the accepted answer to How to initialize const members of structs on the heap
, which apparently is conformant. I do not understand how my example would therefore be non-conformant.
The const-qualifiers on members let the compiler assume that - after an object has been initialized - these members must not be altered through any way, and it may optimise code accordingly (cf, for example, #Ajay Brahmakshatriya comment).
So it is essential to distinguish the initialization phase from the subsequent phases where assignments would apply, i.e. from when on may a compiler assume that an object got initialized and has an effective type to rely on.
I think there is a main difference between your example and that in the accepted answer you cited. In this SO answer, the target aggregate object with const-qualified member types is created through malloc:
ImmutablePoint init = { .x = x, .y = y };
ImmutablePoint *p = malloc(sizeof *p);
memcpy(p, &init, sizeof *p);
According to the rules on how a stored value of an object may be accessed (cf. this part of an online c standard draft), the p-target object will get its effective type the first time in the course of performing memcpy; the effective type is then that of the source object init, and the first memcpy on an object that got malloced can be seen as an initialization. Modifying the target object's const members afterwards, however, would be UB then (I think that even a second memcpy would be UB, but that's probably opinion based).
6.5 Expressions
The effective type of an object for an access to its stored value is the declared type of the object, if any.87) ... If a value is
copied into an object having no declared type using memcpy or memmove,
or is copied as an array of character type, then the effective type of
the modified object for that access and for subsequent accesses that
do not modify the value is the effective type of the object from which
the value is copied, if it has one. For all other accesses to an
object having no declared type, the effective type of the object is
simply the type of the lvalue used for the access.
87) Allocated objects have no declared type.
In your example, however, the target object dst already has a declared type through it's definition struct vector dst;. Hence, the const-qualifiers on dst's members are already in place before the memcpy is applied, and it has to be seen as an assignment rather than an initialization.
So I'd vote for UB in this case.

Can you use restrict-ed pointers to access the same object in some cases?

Most definitions of restrict say that it's a promise from the programmer to the compiler that for the lifetime of the pointer, the pointer is the only way that object is accessed. This allows the compiler to optimize the output, because it knows that it will be accessed only by one pointer and thus can be changed only by it. Which if I understand correctly usually means that the program doesn't have to reload the value the pointer points to.
If this is correct then there should be some exceptions when the restrict keyword should be usable even if it goes against the intent of how it should be used.
One thing that comes to mind would be when the data the pointer points to never actually changes during the lifetime of the pointer. In such case there is no need to reload the data even if the pointers point to the same location, because they don't change in the lifetime of the pointers. E.g.:
int max(int *restrict a, int *restrict b) {
return((*a > *b) ? *a : *b);
}
int main(void) {
int num = 3;
int max = max(&num, &num);
}
Is this a valid use of restrict even though it goes against how it was supposed to be used? Will using the restrict keyword like this result in undefined behaviour?
As Eric says the in a comment that is now gone, the key phrase from the C99 draft standard 6.7.3.1 Formal definition of restrict is:
`If… X is also modified…`
this interpretation is supported by this example in 6.7.3.1/10:
void h(int n, int * restrict p, int * restrict q, int * restrict r)
{
int i;
for (i = 0; i < n; i++)
p[i] = q[i] + r[i];
}
and the following comment with the code sample:
illustrate how an unmodified object can be aliased through two restricted pointers. In particular, if a and b are disjoint arrays, a call of the form h(100, a, b, b) has defined behavior, because array b is not modified within function h.
So it would seem that your specific example is defined behavior since you are not modifying a or b.
You may sometimes use restrict-qualified pointers to access the same objects as other pointers, but only if the pointed-to objects are not modified. Here are C 2011 (N1570) 6.7.3.1 paragraphs 1-3 and the first part of paragraph 4 interspersed with how they apply to the code in the question.
6.7.3.1 Formal definition of restrict
1 Let D be a declaration of an ordinary identifier that provides a means of designating an object P as a restrict-qualified pointer to type T.
So int * restrict a is such a declaration D. When max is called with max(&num, &num);, the object P is num (or, more formally, the object named by num), and T is int. Similarly, int * restrict b is another such declaration.
2 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).
These declarations appear in the parameter declarations of a function definition, so B is the block of the function definition, that is, the body of max.
3 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.137) Note that ‘‘based’’ is defined only for expressions with pointer types.
The function max contains pointer expressions a and b, twice each, so these are each an instance of a pointer expression E. These expressions depend on the parameters a and b, respectively, because if we changed a to point to a copy of num instead of pointing to num, then a would have a different value (obviously), and similarly for b. (Although num is a scalar object, it acts like an array containing a single element for purposes of pointer arithmetic.)
4 During each execution of B, let L be any lvalue that has &L based on P. If L is used to access the value of the object X that it designates, and X is also modified (by any means), then the following requirements apply:…
During the execution of max, the lvalue *a has its address (&*a, which is a) based on P (a), so the lvalue *a is an instance of L. This lvalue is used to access num, so num is an instance of an object X. However num is never modified during the execution of max. Therefore, the requirements that follow do not apply. Similarly the lvalue *b refers to an object (num) that is never modified during the execution of max.
Therefore, the code in max does not violate the requirements for restrict, and its behavior is defined by the C standard.

For struct variables s1,s2,why can I initialize "s1={25,3.5}",assign s2 as "s1=s2",but then can't use "s1={59,3.14}?

In C we are allowed to assign the value of one structure variable to other if they are of the same type.In accordance with that, in my following program I am allowed to use s1=s2 when both are struct variables of the same type.But why then I am not allowed to use s1={59,3.14} after that?
I know we can't assign a string "Test" to a character array arr other than in the initialization statement because for the string "Test",it decomposes to type char* during assignment and hence there is a type mismatch error.But in my program, {59,3.14} doesn't decompose to any pointer,does it?Why then it is not allowed to be assigned to s1 even though it is of same type,especially since it is allowed during the initialization?What is the different between s2 and {59,3.14} such that one is allowed to be assigned to s1 but the other is not?
#include<stdio.h>
int main(void)
{
struct test1
{
int a;
float b;
} s1= {25,3.5},s2= {38,9.25};
printf("%d,%f\n",s1.a,s1.b);
s1=s2; // Successful
printf("%d,%f\n",s1.a,s1.b);
s1= {59,3.14}; //ERROR:expected expression before '{' token|
printf("%d,%f\n",s1.a,s1.b);
}
The C grammar strictly distinguishes between assignment and initialization.
For initialization it is clear what the type on the right side ought to be: the type of the object that is declared. So the initializer notation is unambiguous; { a, b, c } are the fields in declaration order.
For assignment things are less clear. An assignment expression X = Y first evaluates both subexpressions (X and Y), looks at their types and then does the necessary conversions, if possible, from the type of Y to the type of X. An expression of the form { a, b, c } has no type, so the mechanism doesn't work.
The construct that yoones uses in his answer is yet another animal, called compound literal. This is a way of creating an unnamed auxiliary object of the specified type. You may use it in initializations or any other place where you'd want to use a temporary object. The storage class and lifetime of a compound literal is deduced from the context where it is used. If it is in function scope, it is automatic (on the "stack") as would be a normal variable that would be declared in the same block, only that it doesn't have a name. If it is used in file scope (intialization of a "global" variable, e.g) is has static storage duration and a lifetime that is the whole duration of the program execution.
You need to cast it this way: s1 = (struct test1){59, 3.14}; to let the compiler know that it should consider your {...} of type struct test1.
Put in an other way, your data gathered between brackets doesn't have a type, that's why you need to specify one using a cast.
Edit:
The compiler needs to know the expected type for each struct's fields. This is needed to know the right number of bytes for each argument, for padding, etc. Otherwise it could as well copy the value 59 (which is meant to be an int) as a char since it's a value that fits in one byte.

Resources