6

Is there any way to validate on compile time in a c macro that an argument is an array ?

e.g in this two macros:

#define CLEAN_ARRAY(arr) \
    do { \
        bzero(arr, sizeof(arr)); \
    } while (0)

And

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

I tried something using CTC(X) macro , but couldn't find any way to validate/warn if arr isn't an array.

0x90
  • 39,472
  • 36
  • 165
  • 245
  • 1
    I notice you have both `c` and `c++` tags. There are some nice template approaches in `c++`. Do you need an answer suitable for both languages at once? – BoBTFish May 28 '13 at 14:43
  • 1
    sizeof((x)[0]) will give an error if the type is not an "indexable" type - however, if it happens to be a pointer, it will happily take that. And of course, if there is an `operator[]` for the type of `x`. – Mats Petersson May 28 '13 at 14:44
  • Yes, but it can be 2 different approaches. – 0x90 May 28 '13 at 14:44
  • Take a look at http://stackoverflow.com/questions/3385515/static-assert-in-c and http://stackoverflow.com/questions/9369789/parameter-check-in-c-macro – bn. May 28 '13 at 14:45
  • What problem are you trying to solve? Determining whether something is an array, or getting the number of elements of something that is? – juanchopanza May 28 '13 at 14:46
  • For your `ARRAY_SIZE` macro you can have a look at [this article](http://blog.natekohl.net/making-countof-suck-less/) (C++ only). – gx_ May 28 '13 at 14:55
  • @MatsPetersson can you please add your comment as an answer ? – 0x90 May 28 '13 at 15:06
  • 3
    @0x90: For questions that have different answers for C and C++, you should ask separate questions, because you can mark only one answer as accepted, but there may be separate answers for the separate languages. – Eric Postpischil May 28 '13 at 15:11
  • For `ARRAY_SIZE` in `C` see [stackoverflow.com/questions/19452971/array-size-macro-that-rejects-pointers](https://stackoverflow.com/questions/19452971/array-size-macro-that-rejects-pointers) – T S Aug 25 '17 at 10:37

7 Answers7

12

Here's a solution in pure C which invokes no undefined behavior:

#define IS_INDEXABLE(arg) (sizeof(arg[0]))
#define IS_ARRAY(arg) (IS_INDEXABLE(arg) && (((void *) &arg) == ((void *) arg)))

If you need to ensure that the value is an array (then cause a compile time error if not), you can simply use it as an initializer to an enum statement (or a static variable), like this:

static int __ ## arg ## _is_array = IS_ARRAY(arg); // works for an array, fails for pointer.

I'm not entirely sure what will happen with VLA's, but playing around a bit should find that answer out rather fast.


Old answers:

Since this is tagged C (and GCC), I will attempt a solution here:

#define IS_ARRAY(arg) __builtin_choose_expr(__builtin_types_compatible_p(typeof(arg[0]) [], typeof(arg)), 1, 0)

Another solution, using C11's _Generic feature & typeof:

#define IS_ARRAY(arg) _Generic((arg),\
    typeof(arg[0]) *: 0,\
    typeof(arg[0]) [sizeof(arg) / sizeof(arg[0])]: 1\
)

Basically, all it does is use some fancy features of GCC to determine if the type of the argument is compatible with an array of the type of the argument's elements. It will return 0 or 1, and you could replace the 0 with something that creates a compile time error if you wish.

Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
  • The current first method in this answer is good for practical purposes but is technically incorrect as it fails for `x` defined with `void *x = &x;`. – Eric Postpischil May 28 '13 at 16:20
  • @EricPostpischil technically correct: the best kind of correct. Unfortunately I don't think there's any way about that, even if using weirdness with `restrict` pointers. – Richard J. Ross III May 28 '13 at 16:21
  • @EricPostpischil I can't find anything in particular about it, other than the type of the expression shall be `T (*)[size]`. I would assume that in most implementations the address remains the same, but I don't have all day to pour over the standard. – Richard J. Ross III May 28 '13 at 16:29
  • What is the advantage of `__builtin_choose_expr(...,1,0)` over just `__builtin_types_compatible_p(...)`? – T S Aug 25 '17 at 10:50
  • @TS I doubt there's any technical difference in this case. I wrote this answer 4 years ago now, but I think my initial solution (before posting) involved some additional logic based on whether or not the type check succeeded. I then realized that the additional checks were rather redundant. – Richard J. Ross III Sep 11 '17 at 19:52
6

A pure C99 solution:

enum { must_be_an_array_1 = ((void *) &(arr)) == ((void *) (arr)) };
typedef char must_be_an_array_2[((void *) &(arr)) == ((void *) (arr)) ? 1 : -1];

This exploits the fact that the address of an array is the same as the address of its first member, and that an enum member must be an integral constant. If the compiler is smart enough to tell that a pointer-to-a-pointer has a distinct address, it will choke on the second statement.

We still need the first statement because otherwise a compiler with support for runtime sized arrays (e.g. gcc 4.7) will perform the address comparison at runtime and invoke undefined behaviour as the size of the runtime array is negative (e.g. under gcc the program segfaults).

Full program:

#include <strings.h>

#define CLEAN_ARRAY(arr) \
    do { \
        enum { must_be_an_array_1 = ((void *) &(arr)) == ((void *) (arr)) }; \
        typedef char must_be_an_array_2[((void *) &(arr)) == ((void *) (arr)) ? 1 : -1]; \
        bzero(arr, sizeof(arr)); \
    } while (0)

int main() {
    int arr[5];
    CLEAN_ARRAY(arr);
    int *ptr;
    CLEAN_ARRAY(ptr);  // error: enumerator value for ‘must_be_an_array’ is not an integer constant
    return 0;
}
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 2
    I suspect this invokes UB, technically. Pointer arithmetic is only defined for subscripted elements of the same array object. But other than that, this is pretty neat! – Oliver Charlesworth May 28 '13 at 15:28
  • Nice ninja-copy from my answer :) – Richard J. Ross III May 28 '13 at 15:45
  • @OliCharlesworth updated to use `==`, which I think is OK. – ecatmur May 28 '13 at 15:47
  • @RichardJ.RossIII not at all, I'd been working on using `==`. The problem I was having with that was that gcc allows runtime sized arrays. – ecatmur May 28 '13 at 15:47
  • I'm kidding man, it's all CC-wiki anyways. +1'd. – Richard J. Ross III May 28 '13 at 15:48
  • I think this is still implementation-defined (see the rules for constant expressions in section 6.6 p7). – Oliver Charlesworth May 28 '13 at 15:51
  • 1
    @EricPostpischil just because it is not *guaranteed* doesn't mean we can't make assumptions about it. If you want to play the standards game, you can do that all day, but it really means jack if the compiler does something different. – Richard J. Ross III May 28 '13 at 16:31
  • @EricPostpischil: 6.5.9 p6 says "including a pointer to an object and a subobject at its beginning"... Not sure how this interacts with the cast to `void *` though. – Oliver Charlesworth May 28 '13 at 16:31
  • @RichardJ.RossIII: For this sort of thing, a portable solution is infinitely preferable to a non-portable solution that may fail silently on some platforms ;) – Oliver Charlesworth May 28 '13 at 16:32
  • @OliCharlesworth of course, but this type of issue would be easily caught with unit testing. – Richard J. Ross III May 28 '13 at 16:35
  • @RichardJ.RossIII: In this case, it probably would be, yes (assuming you ran your test suite with every compiler/platform you were interested in). But in general, unit tests are a poor way of detecting UB (which this isn't, to be fair). – Oliver Charlesworth May 28 '13 at 16:36
  • 1
    The text in C11 6.6 "Constant expressions" doesn't appear to say that `==` operator with two address constants produces a constant expression usable in an initializer. Although this code works for me in some cases, it must be under /10 (i.e. implementation may define others). – M.M Apr 11 '17 at 14:45
  • Also, the typedef doesn't seem to be required if you are just looking for a compile-time error (the const-ness of the enum initializer is sufficient for this) – M.M Apr 11 '17 at 14:50
  • @M.M yes, looks like it's not a valid arithmetic constant expression under 6.8. The typedef is needed to catch the case where the compiler resolves (under 6.10) the `==` expression to `false`. – ecatmur Apr 18 '17 at 12:51
4

How to validate in c macro the argument is of ARRAY type

Use std::is_array inside of the macro. Or forget themacro and just use std::is_array.

Concerning ARRAY_SIZE,

constexpr size_t size(T const (&)[N])
{
  return N;
}
juanchopanza
  • 223,364
  • 34
  • 402
  • 480
  • 2
    @EricPostpischil it specifically asks about a C macro, it doesn't say anything that would lead me to think it is restricted to C. In any case, if it is tagged C++, I will give a C++ answer. – juanchopanza May 28 '13 at 15:03
  • @EricPostpischil I updated the question accordingly . – 0x90 May 28 '13 at 15:06
  • @EricPostpischil If you check the comments on the question, they want answers for both, and they "can be 2 different approaches." So this answers at least half the question. – BoBTFish May 28 '13 at 15:07
1

(for C++) my current clear macro in VS2010 is:

#define CLEAR(v)    do { __pragma(warning(suppress: 4127 4836)) typedef std::remove_reference< decltype(v)>::type T; static_assert( std::is_pod<T>::value || (__has_trivial_constructor(T) && __has_trivial_destructor(T)), "must not CLEAR a non-POD!" ); static_assert( !std::is_pointer<T>::value, "pointer passed to CLEAR!" );  memset(&(v), 0, sizeof(v)); } while(0)

You can make up your variant using stuff in type_traits header.

A formatted verision with explanations:

#define CLEAR(v) \
do { \ 
    __pragma(warning(suppress: 4127 4836)) \
    typedef std::remove_reference< decltype(v)>::type T; \
    static_assert( \
        std::is_pod<T>::value \
        || (__has_trivial_constructor(T) && __has_trivial_destructor(T)), \
        "must not CLEAR a non-POD!" ); \
     static_assert( !std::is_pointer<T>::value, "pointer passed to CLEAR!" ); \
     memset(&(v), 0, sizeof(v)); \
  } while(0)

outer do-while to make it usable like a genuine function in all places including if/else.

Remove_reference is needed so it works with lvalues, decltype alone makes int* and int*& different and is_pointer reports false for the latter.

The is_pod check is good for general, the additional condition allows struct A1 : A; case work where A is POD and A1 adds only more POD members. For is_pod purpose it's false, but to clear it makes the same sense.

is_pointer check guards the expected mistype when you get the indirection wrong on pointer or pass an address of struct in confusion. Use = NULL to clear a pointer please. ;-)

The __pragma is there to suppress L4 warnings that are issued otherwise.

Balog Pal
  • 16,195
  • 2
  • 23
  • 37
1

As per my comment:

sizeof((x)[0]) will give an error if the type is not an "indexable" type - however, if it happens to be a pointer, it will happily take that. And also, if there is an operator[] for the type of x.

It is quite hard in C to do this, but C++ may allow some template type solutions (I don't actually know how to do that, as I've never tried to do that, or anything similar with templates).

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
1

As far as I can tell, nobody has yet provided a way to ensure that the argument of ARRAY_SIZE is actually an array.

<Edit>
Found Array-size macro that rejects pointers
Original answer follows:
</Edit>

The macro I use for this is:

#define ASSERT_EXPR(condition,return_value) \
(((char(*)[(condition)?1:-1])0)?(return_value):(return_value))

Principle:
0 is casted to pointer to array (with size of one (condition true) or minus one (condition false, generate error)). This null-pointer is then used as the condition of a ternary operator. Although we know, that it will always only evaluate the third operand (null-pointer means false), the second operand is also return_value -- this way the resulting type is the same as the type of return_value.

Using this, (and the IS_ARRAY from Richard J. Ross III's answer) I can define my safe ARRAY_SIZE macro as follows:

#define IS_ARRAY(arg) __builtin_choose_expr(__builtin_types_compatible_p(typeof(arg[0]) [], typeof(arg)), 1, 0)
#define ARRAY_SIZE(x) ASSERT_EXPR(IS_ARRAY(x), (sizeof(x)/sizeof((x)[0])) )

I didn't manage to get it working with Richard J. Ross III's two other IS_ARRAY variants, but that might be my (or gcc's) fault...

T S
  • 1,656
  • 18
  • 26
-3

In C, this should work:

#define VALIDATE_ARRAY(arr) (void)(sizeof((arr)[0]))

int *a, b;
int main() {
        VALIDATE_ARRAY(a);
        VALIDATE_ARRAY(b);
        return 0;
}

This program will fail to compile, because b is not an array. This is because b[0] is an invalid. It won't distinguish pointers from arrays - I don't think you can do that.

This form of the macro can only be used inside a function. If you want to use it outside a function, you'll have to modify it (e.g. to declare an extern array).

ugoren
  • 16,023
  • 3
  • 35
  • 65