Compile-time consistency checks for types in C
July 6, 2015
Walkthrough of a compile-time consistency check for types in C language
Compile-time consistency checks
Say that in existing source code, you happened upon the construct below:
#define CHECKED_TYPE(original_type, p) ((conversion_type*) (1 ? p : (original_type*) 0))
What might the purpose of this strange construct be?
At run-time, the inner conditional expression always evaluates to p, and thus the entire expression evaluates to the conversion to conversion_type* of p. Thus, with regard to what it evaluates to, the macro might as well have been written (conversion_type*) p.
At compile-time, however, a C11 compiler will apply the rules prescribed by the standard for the conditional expression:
6.5.15 Conditional operator
Syntax
1
conditional-expression: … logical-OR-expression ? expression : conditional-expression
…
3 One of the following shall hold for the second and third operands:
— both operands have arithmetic type;
— both operands have the same structure or union type;
— both operands have void type;
— both operands are pointers to qualified or unqualified versions of compatible types;
— one operand is a pointer and the other is a null pointer constant; or
— one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void.
Because the macro hard-codes an expression with type original_type*, cases 1, 2, 3 will never apply. In practice neither the type pointed by p nor original_type is void, so the sixth case does not apply either. That leaves only cases 4 and 5: the pointer p must have compatible types up to qualifiers, or p must be a null pointer constant.
Clause 6.5.15:3 above falls below a Constraints section, which means that a compliant compiler has to emit a diagnostic if a C program doesn’t respect the stated conditions. Thus the macro CHECKED_TYPE(original_type, p) expands to something that causes at least a compiler warning if p is not of a type compatible up to qualifiers with original_type*, or a null pointer constant (e.g. NULL, 0, (void*)0).
In short, the macro CHECKED_TYPE(original_type, p) checks that the pointer p has type original_type* before converting it to the hard-coded type conversion_type*. If the check fails, compilation will likely stop (depending on the compiler, but a compliant compiler has to at least emit a warning). Clang uses an enabled-by-default warning:
$ cat t.c #define CHECKED_TYPE(original_type, p) \ ((void*) (1 ? p : (original_type*) 0)) int main(void) { int x = 1; void *p = CHECKED_TYPE(int, &x); char y = 2; void *q = CHECKED_TYPE(int, &y); float z = 3; void *r = CHECKED_TYPE(int, &z); } $ clang t.c t.c:8:13: warning: pointer type mismatch ('char *' and 'int *') [-Wpointer-type-mismatch] void *q = CHECKED_TYPE(int, &y); ^~~~~~~~~~~~~~~~~~~~~ t.c:2:15: note: expanded from macro 'CHECKED_TYPE' ((void*) (1 ? p : (original_type*) 0)) ^ ~~~~~~~~~~~~~~~~~~ t.c:10:13: warning: pointer type mismatch ('float *' and 'int *') [-Wpointer-type-mismatch] void *r = CHECKED_TYPE(int, &z); ^~~~~~~~~~~~~~~~~~~~~ t.c:2:15: note: expanded from macro 'CHECKED_TYPE' ((void*) (1 ? p : (original_type*) 0)) ^ ~~~~~~~~~~~~~~~~~~ 2 warnings generated.
In real software, this idiom is found in OpenSSL’s source code.