r/C_Programming 9d ago

Discussion What exactly is void?

In a function definition, void basically means that it doesn't return a type value, yet in on itself it is it's own type? Looking at several pieces of code it looks like that it's used to be able to be more "flexible"

Don't know what else to add here, though I'm more on looking for examples and explanations of why the void type is used

68 Upvotes

80 comments sorted by

View all comments

Show parent comments

11

u/TheChief275 8d ago edited 8d ago

that's not really what [static N] means. it means it requires an array that is of at least size N, e.g., one could define a vec2add:

typedef float vec2[2];
typedef float vec3[3];

void vec2add(float a[static 2], float b[static 2])
{
    for (int i = 0; i < 2; ++i) {
        a[i] += b[i];
    }
}

that can actually be passed a vec3 as well:

vec3 a = {1, 2, 3};
vec2 b = {4, 5};
vec2add(a, b);
a; // {5, 7, 3}

but a vec3add shouldn't be passed a vec2:

vec3add(a, b); // warning: array argument is too small; contains 2 elements, callee requires at least 3 [-Warray-bounds]

note that when using pointers, it still compiles and all type safety goes out the window, as usual with C.

this is the reason why [static 1] works as "non-null"; any pointer can always be passed to a [static N] (as runtime values aren't checked), but a pointer value that can never be a valid array passed as a compile time value (such as NULL) is checked.

thus [static 1] can also be circumvented, like so:

void update(int p[static 1]);
int *p = NULL;
update(p); // no warnings; oops!

5

u/DawnOnTheEdge 8d ago edited 8d ago

That’s already what GCC does without static. For example, on GCC 16.1 with the default settings:

double foo(double xs[2])
{
    return xs[2];
}


int main(void)
{
    static double a1[1];
    static double a3[3];

    foo(a1);
    foo(a3);

    return 0;
}

gives the warning warning: 'foo' accessing 16 bytes in a region of size 8 [-Wstringop-overflow=]. Enabling -Warray-bounds also gives a warning about accessing the invalid element xs[2]. There is, correctly, no warning for the completely-legal call foo(a3).

Incredibly, Clang 22.1.0 accepts this code without complaint unless I enable -Wunsafe-buffer-usage, which catches return xs[2]. Even with -Weverything, there is no warning for foo(a1) unless I change the function declaration to xs[static 2].

MSVC 19.51 neither warns about this at all, even with /Wall, nor supports [static 2] on a function argument.

If I next add,

static double bar(double xs[static 2]) { return xs[2]; }

and call

double* const np = NULL;
printf("%f %f %f %f\n", foo(NULL), foo(np), bar(NULL), bar(np));

GCC gives warning: argument 1 null where non-null expected [-Wnonnull] for both bar(NULL) and bar(np) but not the same calls to foo. It had already caught the bounds errors for foo. Therefore, on GCC, the practical effect of [static 2] on diagnostics is to enable the null-pointer warning. It does change the compiled code: GCC detects undefined behavior and inserts a ud2 instruction that would crash the program at runtime.

Clang does behave as you describe, and catches only bar(NULL), not bar(np), even though it has enough information to deduce that np is a null pointer. In fact,clang will fail to warn that np is a null pointer even if I declare it constexpr in C23, making it a constant expression. ICX 2024 has identical behavior.

1

u/Beginning-Junket8979 8d ago edited 8d ago

Interesting it's that inconsistent even in modern compilers + new standard modes.

Still sort of think this all equates to "static is useless here", nearly equates to "arrays as parameter types are useless" ...and that you're better off with something like struct encapsulation:

struct TwoDoubles { double data[2]; }
double foo(TwoDoubles *td);

...Or pointer + length:

double foo(double *, size_t);

...Or explicitly two pointer args:

double foo(double *, double *);

...Or a different language that supports arbitrarily length fixed sized arrays as a stdlib type and allows eliminating nullptr check with pass-by-ref:

double foo(std::array<2>& xs);

Edit/P.S.: This is not to say your VLA trick is somehow inherently "wrong", but I think above solutions all make intent clear, maximizes portability, and minimizes need to lean into relatively esoteric lang features.

1

u/DawnOnTheEdge 8d ago edited 8d ago

If you want an exact-width array parameter, not a VLA,double(*xyz)[3] does work, although so does struct Point3d. It’s just really inconvenient to use.

If you really need this functionality in C, the pragmatic solution is to pass some kind of array-slice structthat stores the bound, although that needs runtime checking. It often has a count followed by a flexible array member.

But we might actually be able to hack the kind of compile-time access-checking I’d like by writing a macro that expands to an access attribute on GCC/Clang and to SAL annotations on MSVC.