15

Is there some "nice" way to check if a variable passed to a macro is a pointer? e.g.

#define IS_PTR(x) something
int a;
#if IS_PTR(a)
printf("a pointer we have\n");
#else
printf("not a pointer we have\n");
#endif

The idea is that this is not done run-time but compile-time, as in: we get different code depending on if the variable is a pointer or not. So I would like IS_PTR() to evaluate to some kind of constant expression in some way. Am I going about this idea all the wrong way?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
lfxgroove
  • 3,778
  • 2
  • 23
  • 33
  • @mbratch, modern C (AKA C11) has means to observe types, namely the new operator `_Generic`. – Jens Gustedt Oct 08 '13 at 18:15
  • I've tried to clarify what i meant as i wasn't thinking of the possibility of trying to do this run time, hopefully it's more clear what i mean now. – lfxgroove Oct 08 '13 at 18:16

6 Answers6

8

On Clang and GCC, __builtin_classify_type(P) evaluates to 5 if P is an object with a pointer type.

zneak
  • 134,922
  • 42
  • 253
  • 328
7

It is certainly not observable through the preprocessor in #if as you imply in your question. The preprocessor knows nothing about types, only tokens and expressions that are constructed from them.

C11 has a new feature that lets you observe a particular pointer type, but not "pointerness" in general. E.g you could do something

#define IS_TOTOP(X) _Generic((X), default: 0, struct toto*: 1)

or if you'd want that the macro also works for arrays

#define IS_TOTOPA(X) _Generic((X)+0, default: 0, struct toto*: 1)

There are already some compilers around that implement this, namely clang, and for gcc and others you can already emulate that feature with some builtins, see P99.

Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
6

You can get a complete answer to this by combining what @alx and @zneak have, and throwing in __builtin_choose_expr:

#define is_same_type(a, b)  __builtin_types_compatible_p(typeof(a), typeof(b))

#define is_pointer_or_array(p)  (__builtin_classify_type(p) == 5)

#define decay(p)  (&*__builtin_choose_expr(is_pointer_or_array(p), p, NULL))

#define is_pointer(p)  is_same_type(p, decay(p))

So, __builtin_choose_expr(expr, a, b) is equivalent to expr ? a : b, except that the types of a and b don't need to be compatible, and there are no conversions done. In case the input is not a pointer nor an array, NULL will be of different type, so is_pointer() will evaluate to false.

If we use the demo supplied by @Netherwire with a few tweaks, we'll see that the above can't handle incomplete structs (compile error). Still, we get no false-positives while also not failing on non-pointers/arrays. I think this should be good enough for most use-cases.

Here's @Netherwire's demo, with a few modifications:

char c;
printf("c is a pointer: %s\n", is_pointer(c) ? "Yes" : "No");

unsigned long long ll;
printf("ll is a pointer: %s\n", is_pointer(ll) ? "Yes" : "No");

double d;
printf("d is a pointer: %s\n", is_pointer(d) ? "Yes" : "No");

unsigned char *cp;
printf("cp is a pointer: %s\n", is_pointer(cp) ? "Yes" : "No");

struct TM *tp;
printf("tp is a pointer: %s\n", is_pointer(tp) ? "Yes" : "No");
// printf("*tp is a pointer: %s\n", is_pointer(*tp) ? "Yes" : "No");  // error

char a[42];
printf("a is a pointer: %s\n", is_pointer(a) ? "Yes" : "No");

printf("1 is a pointer: %s\n", is_pointer(1) ? "Yes" : "No");

printf("\"str\" is a pointer: %s\n", is_pointer("str") ? "Yes" : "No");

This outputs:

c is a pointer: No
ll is a pointer: No
d is a pointer: No
cp is a pointer: Yes
tp is a pointer: Yes
a is a pointer: No
1 is a pointer: No
"str" is a pointer: No

Huzzah! We have no false-positives, plus the defines were able to handle more than just arrays.

  • You don't really need an `UNREACHABLE_VAR`. You can directly use `NULL` as a random pointer. – alx - recommends codidact Oct 21 '22 at 15:53
  • For improving readability, I suggest breaking that huge macro into smaller ones: `#define is_same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))` `#define is_ptr_or_array(p) (__builtin_classify_type(p) == 5)` `#define decay(a) (&*__builtin_choose_expr(is_ptr_or_array(a), a, NULL))` `#define is_ptr_not_array(p) is_same_type(p, decay(p))` `#define is_pointer(p) (is_ptr_or_array(p) ? is_ptr_not_array(p) : false)` – alx - recommends codidact Oct 21 '22 at 15:54
  • Breaking into smaller macros made me realize that you can simplify: `#define is_same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))` `#define is_ptr_or_array(p) (__builtin_classify_type(p) == 5)` `#define decay(a) (&*__builtin_choose_expr(is_ptr_or_array(a), a, NULL))` `#define is_pointer(p) is_same_type(p, decay(p))` – alx - recommends codidact Oct 21 '22 at 16:01
  • Yea, I appreciate you taking it even farther! I'm surprised you were able to get rid of the redundant __builtin_choose_expr, because I kept getting errors when attempting that. Definitely looks a lot cleaner now! – Kyle Ponikiewski Oct 28 '22 at 16:51
5

I found a more or less _Generic solution of this problem.

Warning: May be triggered false-positive (see an example below).

#define __INTERNAL_CHECK_POINTER(x) _Generic((x),\
          int: 0,       unsigned int: 0,\
         long: 0,      unsigned long: 0,\
    long long: 0, unsigned long long: 0,\
        float: 0,             double: 0,\
  long double: 0,                       \
      default: 1)

/**
 * Determines whether the variable has likely a pointer type (but may be triggered false-positive)
 */
#define IS_LIKELY_A_POINTER(x) ((sizeof(x) == sizeof(void*)) && __INTERNAL_CHECK_POINTER(x) ? 1 : 0)

Demo:

char c = 0;
printf("c is a pointer: %s\n", IS_LIKELY_A_POINTER(c) ? "Yes" : "No");

unsigned long long l = 0;
printf("l is a pointer: %s\n", IS_LIKELY_A_POINTER(l) ? "Yes" : "No");

double d = 0.0;
printf("d is a pointer: %s\n", IS_LIKELY_A_POINTER(d) ? "Yes" : "No");

unsigned char* cp = 0;
printf("cp is a pointer: %s\n", IS_LIKELY_A_POINTER(cp) ? "Yes" : "No");

struct tm* tp = 0;
printf("tp is a pointer: %s\n", IS_LIKELY_A_POINTER(tp) ? "Yes" : "No");

char ia[] = {0, 1, 2, 3, 4, 5, 6, 7};
printf("ia is a pointer: %s\n", IS_LIKELY_A_POINTER(ia) ? "Yes" : "No");

This will output:

c is a pointer: No
l is a pointer: No
d is a pointer: No
cp is a pointer: Yes
tp is a pointer: Yes
ia is a pointer: Yes // false-positive!

If you (like me) are looking for some logging (to draw or not to draw a * for a particular variable) and you're not looking for fail-proof results, try this, it may help. Cheers!

NOTE that it won't compile under MSVC; use gcc/clang/etc. instead or make your own fallback implementation using this condition:

#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
    // use _Generic code
#else
    // ¯\_(ツ)_/¯
#endif
Netherwire
  • 2,669
  • 3
  • 31
  • 54
3

NULL is pretty much the only thing you can look for. There is no way to determine if something is a pointer.

Jim
  • 1,056
  • 1
  • 13
  • 19
1

There is a way without false positives nor negatives, but compilation will break if the parameter is neither an array nor a pointer.

#define is_same_type(a, b)    __builtin_types_compatible_p(typeof(a), typeof(b))
/* Is a an array? */
#define is_array(a)             (!is_same_type((a), &(a)[0]))
/* Is p a pointer? */
#define is_pointer(p)           (!is_array(p))

It needs the GCC builtin __builtin_types_compatible_p().

Behaviour:

If p is an array, it will evaluate as 0;

Else if p is a pointer, it will evaluate as 1;

Else, compilation will break.