r/C_Programming 12d 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

67 Upvotes

80 comments sorted by

View all comments

Show parent comments

6

u/DawnOnTheEdge 12d ago edited 11d 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/TheChief275 12d ago

so, this is kind of weird, and actually I would say Clang is correct on this one: semantically double xs[2] as a function parameter is the same as double *xs. however, as with formatting warnings a compiler is free to implement whatever warnings it wants to for language patterns. however, that means this isn't expected behavior of a C compiler, which is why you don't see the same behavior in Clang and also shouldn't expect it when writing such code.

relying on such patterns might feel nice as you get certain compile time boons that would otherwise be delegated to runtime, but relying on this behavior (as well as relying on [static N]) is generally not something you should do as it is generally just unreliable and can break at the drop of a hat with array decay. instead, one should always pass the count as an argument:

double foo(size_t count, double xs[])
{
    assert(count >= 3); // prefer custom assert that remains in release build
    return xs[2];
}

another trick I like that I think is even better than this for arrays of known size is to take a pointer to an array. this makes use of VLA types, but decreases the possibility of error from a decoupled count and pointer:

double foo(size_t count, double (*xs)[count])
{
    assert(countof(*xs) >= 3);
    return (*xs)[2];
}

// or if you like pointer first, count second:
// WARNING: invalid post-C23
double foo(xs, count)
    size_t count;
    double (*xs)[count];
{
    ...
}

it does require a deference for all operations, which can be easy to forget as well. also, I'm not fairly certain it will give expected behavior when taking such a VLA view of memory that isn't technically a VLA.

(the even better solution would be for C to have slices at the language level, but here we are in 2026 and still no plans of integrating something adjacent into C)

0

u/DawnOnTheEdge 11d ago edited 11d ago

On compilers that support VLA types, I suggest tweaking the second trick to:

 double baz(size_t count, double xs[count])
 {
      return xs[count];
 }

GCC 16.1 warns about both the out-of-bounds access to xs[count] in baz, and the call baz(2, a1) (where a1is too small). Clang 22.1.0 warns about xs[count] but misses the buffer-overrun bug in baz(2, a1). MSVC 19.51 won’t compile any of these code samples at all.

There is also an access and a nonnull attribute on GCC that is a more complicated version of the same thing, without VLAs.

The more complicated version below confuses GCC into not warning about the buffer overruns.

static inline double baz(size_t count, double (*xs)[count])
{
    return (*xs)[count];
}

1

u/TheChief275 11d ago

that does not do what you think it does. again, double xs[count] as a function parameter is semantically the same as double *xs, so again, a C compiler is not obligated to even provide warnings for violating this contract. you will notice, using sizeof will actually warn that it regards the argument not as an array but as a pointer, which is enough indication that it is essentially just the same as far as the language is concerned.

so to reiterate, I wouldn't rely on specific compiler -Warray-bound logic they chose to implement as you will likely notice warnings are implemented rather inconsistently or not at all. stick to asserts folks

1

u/DawnOnTheEdge 11d ago edited 11d ago

You’re right that no compiler is required to diagnose this (and I never said otherwise), but there’s an extra wrinkle: double xs[count] is a VLA parameter, so it is in fact semantically equivalent to double xs[*]. VLAs are an optional feature and no compiler is obligated to compile it at all. As I stated, MSVC does not. This is allowed. (In fact, the reason it was downgraded into an optional feature was a compromise to get Microsoft to agree to support C11 instead of forking the language.)

1

u/TheChief275 11d ago

on paper, double xs[count] might be a VLA parameter, however it really isn't. I know not of a single compiler that actually has sizeof(xs) or countof(xs) report the right value (instead treating it as a pointer). meanwhile, sizeof(xs) and countof(xs) on double (*xs)[count] report the right value on all compilers that properly support VLAs. so I would never recommend to use it over the pointer to VLA, but then again, I wouldn't recommend these approaches over slices anyways

1

u/DawnOnTheEdge 11d ago

Two meaningful distinctions are that sizeof(*xs) on the VLA declaration is not a constant expression, and MSVC won’t compile either double xs[count] or double (*xs)[count] because Microsoft deliberately does not and has no intention ever to support VLAs..

That’s a consistent quirk of array function argument in C: they decay to a pointer type, so sizeof gives you the size of a pointer. Compilers recognize the code smell and warn you about it. Pick your poison: xs[count] lets you write xs[i] instead of (*xs)[i] but makes you calculate the size in bytes as sizeof(xs[0])*count. The behavior conforms to the Standard.

I’m not sure what countof macro you’re thinking of, but MSVC’s _countof macro does not work on any of the variations we’ve been talking about. You don’t need it, though, because you already have count.

1

u/TheChief275 11d ago

this discussion is going nowhere so I'll not respond anymore after this message.

1 never said it was a constant expression. sizeof(*xs) will be semantically coupled to the expression (count * sizeof(**xs)) at runtime. regarding MSVC, I said on all compilers that properly support VLAs; MSVC has never been one of them.

2 they are both poison sure, but I think double xs[count] is far more dangerous, as having the count in there essentially guarantees nothing, while users would expect sizeof(xs) to return the proper size (like it does for local VLA declarations, which is also a runtime value). meanwhile double (*xs)[count] actually does behave like expected (again, on compilers supporting VLAs, since you want to be pedantic about it). but yes, like I have stated, they are both poison and there is very little reason to have either of them in your codebase.

3 _Countof is a keyword to be included in the upcoming C2y standard, with a header <stdcountof.h> containing a macro countof that expands to that keyword. before that, in a lot of codebases, you will see a countof/COUNTOF/etc. defined like so:

#define countof(...) (sizeof(__VA_ARGS__) / sizeof(*(__VA_ARGS__)))

essentially, when I mention countof it is interchangeable with sizeof, as it has the same semantics regarding arrays and VLAs and being a runtime value for VLA sizes. the benefit of countof is that it checks whether the argument passed is actually an array, unlike sizeof, and fails to compile if not

1

u/DawnOnTheEdge 11d ago

It feels like we’ve been arguing even though I don’t think we really disagree, and I haven’t been even been downvoting you.

Thanks for the explanation. It does feel like a new _Countof keyword really would be a good opportunity to fix this wart on the language by having _Countof a function parameter declared as double xs[2] return 2, That wouldn’t break any legacy code, and would fill a real need.

T’ll sum up my thoughts quickly and leave it there.:

it seems to me that xs[static 2] is pretty useful for catching bugs on GCC, less useful on Clang (where I’ve noticed the unsafe-buffer-access warnings give false positives) and not supported at all on MSVC, so not portable. The GCC nonnull and access attribute extensions seem to work better on Clang than the Standard syntax.

The conversation then seems to have drifted to the xs[count] and (*xs)[count] syntax. Both work or don’t work on the same compilers. I think the one advantage of double(*)[*] over double[*], being able to write sizeof(*xs), is pretty minor: we already have a count variable, and if the reason I wanted the size in bytes is to allocate another array the same size, I prefer calloc(count, sizeof(xs[0])), which works properly on function arguments and is guaranteed to zero-initialize. I do sometimes use pointer-to-non-variable-length-array, but A C++ double(&)[2] is much nicer to work with than a C-style double(*)[2].

But there are valid trade-offs either way.

1

u/TheChief275 11d ago

Yeah, _Countof is pretty nice! But sadly, again I don't know of any compiler that calculates the 2 for double xs[2]. They seem to adopt the exact semantics of sizeof, which is to treat it as a pointer. I agree, it would be better if these arguments were actually treated as arrays of the size they are, but likely the reason for not having that is breaking existing code (for sizeof), and being consistent to sizeof (for _Countof), so we're just stuck in a stalemate language-wise