6
$\begingroup$

In C the sizeof() operator cannot be resolved in preprocessor conditions. If other operators such as + or - can be (so long as the operands are constants) then what is different about sizeof()?

What technical limitations prevent sizeof() from being resolved at preprocess time while other operators can be and what would it take for C to permit constructs such as the following?

#if sizeof(int) == 4
// Code
#endif

sizeof() is compile time constant but not preprocess time constant, hence the following is valid:

static char array[sizeof(int)];

If the compiler knows type sizes why can the preprocessor not?

$\endgroup$
8
  • $\begingroup$ In the case of C, struct padding/packing is implementation-defined and I think it depends on compiler flags in modern compilers. So, the preprocessor would have to understand those variables, which it generally doesn’t and in some cases can’t. $\endgroup$
    – Bbrk24
    Jun 8, 2023 at 20:08
  • $\begingroup$ I’ve said this on other posts before, but a lot of the specific decisions C made were to make the compiler simpler — the language is over 50 years old at this point. $\endgroup$
    – Bbrk24
    Jun 8, 2023 at 20:13
  • $\begingroup$ The preprocessor is entirely text-based. It knows nothing about the semantics of the files it's operating on. It can resolve a mathematical expression in an #if, but it cannot possibly understand what a name refers to $\endgroup$
    – abel1502
    Jun 8, 2023 at 21:53
  • $\begingroup$ What are the close votes for here? It seems a clear on-topic question about implementing a C compiler. Insufficient research? $\endgroup$ Jun 9, 2023 at 12:03
  • 1
    $\begingroup$ @BruceAdams Sorry, I linked the wrong meta post. This is the one I had in mind. $\endgroup$
    – Bbrk24
    Jun 9, 2023 at 19:42

2 Answers 2

5
$\begingroup$

In order to evaluate the expression sizeof(T), it is necessary to know what type is meant by T, which in general requires parsing and name resolution (at the type level) to have already been done. In fact, even recognising that sizeof(T) is an expression and that T is a type means that parsing must already have been done. The C preprocessor is run before parsing.

In order for the preprocessor to be able to evaluate expressions like sizeof(T) == 4, there would need to be a back-and-forth between the preprocessor and the compiler; the preprocessor would have to invoke the compiler to parse and evaluate these expressions, and the type declarations they refer to, before continuing to preprocess more source code.

This isn't impossible in principle, but it would be rather complex even just to specify what the correct results are supposed to be. For example, what happens when the preprocessor needs to know the size of a type, but the type's declaration itself can be different depending on another preprocessor condition? What happens if preprocessor conditions are circularly dependent?


So this sort of thing is much easier to do after compilation, using e.g. constant folding and dead code elimination. In languages which compile to Javascript, for example, you might write something like

const someFunction = (SOME_ARRAY.length >= 4 ? function() {
    // ...
} : function() {
    // ...
});

Then when the condition evaluates to a compile-time constant true or false, the compiler ─ or some later build step ─ can simplify this declaration to just the one relevant function body.

$\endgroup$
2
  • $\begingroup$ I don't know much about Zig but this aspect seems like an advantage of compile time evaluation versus preprocessors. In compile time evaluation there is no expansion stage so anything that is known in compile time just works. $\endgroup$
    – Aiono
    Jun 12, 2023 at 17:24
  • $\begingroup$ @Aiono You are correct... but compile-time evaluation is of course more complex to implement, and there are still limitations -- such as recursion/co-recursion leading to unsolvable problems. Nicer to use in general, though, definitely. $\endgroup$ Sep 12, 2023 at 15:29
3
$\begingroup$

C was designed to allow programs to be built on computers with extremely limited memory. This made it necessary to run certain processing phases to completion before loading in code that would perform the next phase. Preprocessing would have required keeping track of a significant number of symbols which could relatively long expansions, and would thus require a fairly large amount of "data" memory, but the preprocessing program itself could be fairly small and simple. A later compilation phase would be more complicated, requiring a bigger program to perform it, but it wouldn't need to keep track of variable-length objects.

What is unfortunate is not that the preprocessor is limited in what it can do, but that there's no mechanism other than the preprocessor via which a program could include or exclude structure fields based upon information (such as object sizes) that would be available to the compiler, but not the preprocessor.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .