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

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.

Newsletter