Authors: Jay Ghiron
Date: 2026-06-30
Submitted against: C23
Status: Open
Cross-references: 0282
Consider the following:
struct S{char x,y[];};
struct S s;
struct S*p=&s+1;
For the purposes of these operators, a pointer to an object that is not an element of an array behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.
(C23 6.5.7 "Additive operators" paragraph 8.)
This wording assumes that for any object of type T which is not part
of an array, there can exist an object whose type is T[1].
However, this assumption is broken with flexible array members:
A structure or union shall not contain a member with incomplete or function type (hence, a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.
(C23 6.7.3.2 "Structure and union specifiers" paragraph 3.)
Moreover, for a pointer one past the end of this hypothetical array to be formed it would need to know the true size of the flexible array member. GCC, Clang, and MSVC all allow the original example but obviously cannot do this so they just advance by the size of the structure. The consequence of this is that the resulting pointer can end up pointing into the flexible array of the object which it should be pointing one past.
No suggested correction is provided due to significant wording changes in N3517 and N3322. The following is a possible resolution to this issue without any specific wording:
typeof_unqual(s) is a complete structure
type or complete union type which in either case cannot be the element
type of an array type.* that points to an object of type s.* which is unequal to a.+0, 0+a, and a-0 are always defined and result in a.+n, n+a, and a-n are always undefined.-a is always defined and results in zero.-b and b-a are always undefined.[0] and 0[a] are always defined and are equivalent to *a.[n] and n[a] are always undefined.Preserving pointer arithmetic with zero seems useful, at least to
continue to keep a[0] valid as used in the idiom &a[0].
Additionally with
N3322
being accepted, it would not make sense to forbid this when even null
pointers allow for pointer arithmetic with zero. Allowing a+1 with
the current behavior given by GCC, Clang, and MSVC does not seem
useful however. For some context, consider the following:
#include<stddef.h>
#include<stdlib.h>
struct T{int i;short s;char c[];};
static_assert(sizeof(struct T)==8);
static_assert(offsetof(struct T,c)==6);
int main(){
struct T*q=malloc(/* ? */);
if(q){
*q->c=0;
free(q);
}
}
Suppose these static_assert declarations succeed, what is the minimum
size to ensure that the behavior of this program is defined? Here
would be three ways of calculating that size:
offsetof(struct T,c)+1 which says the minimum size is seven.offsetof(struct T,c)+1<sizeof(struct T)?sizeof(struct T):offsetof(struct T,c)+1 which says the minimum size is eight.sizeof(struct T)+1 which says the minimum size is nine.The third approach seems definitely safe, as the standard provides
multiple examples using this style. The second approach appears fine
too, though the standard provides the following example in C23
6.7.3.2 (sizeof(double)==8 is assumed to be true):
struct s { int n; double d[]; };
struct s *s1;
struct s *s2;
s1 = malloc(sizeof(struct s) + 10);
s2 = malloc(sizeof(struct s) + 6);
double *dp;
dp = &(s1->d[0]); // valid
*dp = 42; // valid
dp = &(s2->d[0]); // valid
*dp = 42; // undefined behavior
This is really multiple examples combined, since there are multiple
examples which refer back to previous examples. It is stated that the
last *dp=42 is undefined behavior, though it is not clear why it
would be undefined if sizeof(struct s)-offsetof(struct s,d)>=2 is
true. Though such an implementation would be uncommon, perhaps it was
just forgotten as a possibility. The first approach seems to be the
most useful and is also commonly used, but it would not work with
q+1 using the semantics given by GCC, Clang, and MSVC. It would
also not work with assignment to the whole struct T for example
*q=*q, but assignments involving flexible array members do not
actually copy the whole flexible array member so they are useless even
though it is possible to do. And as mentioned in another example,
some of the bytes in the destination flexible array member can even
become indeterminate after copying. If the first approach is
sufficient for the access to *q->c to be valid, it would create a
strange edge case to allow q+1 but not for this specific scenario.